Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
aa801fb
feat(spec): ownership queue and assignment rule contract (ADR-0142)
claude Oct 26, 2026
e99e364
feat(plugin-sharing): ownership-queue service, queue tables and the s…
claude Oct 26, 2026
082f181
feat(objectql): ownership-queue column, write precondition and assign…
claude Oct 26, 2026
1f0377d
feat(plugin-security): claim, release and move in the ownership gate …
claude Oct 26, 2026
95b3e6e
feat(approvals,lint): queue approvers and assignment-rule checks (ADR…
claude Oct 26, 2026
f940a05
feat(runtime,sharing): switch declared queues and assignment rules (A…
claude Oct 26, 2026
11a6513
chore(changeset): ownership queues and assignment rules (ADR-0142)
claude Oct 26, 2026
cbd2548
fix(gates): census, empty-state, llms.txt, field codes and transcript…
claude Oct 26, 2026
c98a59a
fix(gates): logger receiver, double bound, typed driver options, docs…
claude Oct 26, 2026
0388a83
fix(gates): artifact door maps queues, parsed aliases, showcase and s…
claude Oct 26, 2026
7fa35c5
fix(gates): the queue test matcher refuses what it does not implement…
claude Oct 26, 2026
adb8601
chore(spec): record AssignmentRuleEntryParsed in the api-surface and …
claude Oct 26, 2026
defd452
Merge origin/main into claude/project-thread-g33c7k
claude Oct 26, 2026
578e7a4
Merge origin/main into claude/project-thread-g33c7k
claude Oct 26, 2026
9bc508a
fix(plugin-sharing): the shadowed-queue report declares a required wa…
claude Oct 26, 2026
6f98217
feat(spec)!: declare the reserved queue column as a narrowing (ADR-01…
claude Oct 26, 2026
e0cfccd
Merge origin/main into claude/project-thread-g33c7k
claude Oct 26, 2026
4028072
fix(cli): give the three ownership-queue tables an organization-owner…
claude Oct 26, 2026
0f2b6ac
fix(platform-objects): label the Queues and Queue Members setup navig…
claude Oct 26, 2026
2448c00
test: teach the injected queue column and the two code-only queue typ…
claude Oct 26, 2026
9ada3a9
test(driver-sql): admit the text-typed queue anchor in the sort-ancho…
claude Oct 27, 2026
564ef7a
test(objectql): pin the assignment criteria and load fail paths
claude Oct 27, 2026
e451337
test(dogfood): classify the assignment-rule expressions and count the…
claude Oct 27, 2026
45558e2
fix(objectql): an extension's own queue field never replaces the inje…
claude Oct 27, 2026
a554823
Merge origin/main into claude/project-thread-g33c7k
claude Oct 27, 2026
797aeb4
docs(skills): the queue approver type resolves to the queue's members
claude Oct 27, 2026
d1a0d73
Merge origin/main into claude/project-thread-g33c7k
claude Oct 27, 2026
aea6b12
docs(skills): fit the queue approver row under the reference's token …
claude Oct 27, 2026
290a435
Merge origin/main into claude/project-thread-g33c7k
claude Oct 27, 2026
cbd27d9
Merge origin/main into claude/project-thread-g33c7k
claude Oct 27, 2026
f35f411
Merge origin/main into claude/project-thread-g33c7k for a conflict
claude Oct 27, 2026
d6bbeae
Merge origin/main into claude/project-thread-g33c7k for a conflict
claude Oct 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .changeset/25769-ownership-queue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
'@objectstack/spec': minor
'@objectstack/objectql': minor
'@objectstack/metadata-core': minor
'@objectstack/metadata': minor
'@objectstack/plugin-security': minor
'@objectstack/plugin-sharing': minor
'@objectstack/plugin-approvals': minor
'@objectstack/lint': minor
'@objectstack/runtime': minor
'@objectstack/platform-objects': minor
'@object-ui/app-shell': minor
'@objectstack/skills': minor
'@objectstack/cli': minor
---

feat!: ownership queues: records wait in a queue until a member claims them, and assignment rules route new records (ADR-0142)

Clause-②: yes (narrowing)
Metadata-protocol: narrowed

<!-- adr-0087: registered object-queue-field-reserved -->

**BREAKING** for one shape only: an object whose records have a user owner (`ownership: 'user'`, the default) can no longer declare its own field named `queue`. The platform now injects a `queue` column on every such object, and `registerObject` refuses the declaration with `422 INVALID_METADATA` (`ObjectQueueColumnRefusedError`), naming the object. An `objectExtensions` entry that declares `queue` on such an object is not refused, because an extension is folded over its object at read time: its `queue` is ignored, the platform's column stands, and a warning names the extension. The remedy: rename the business field (for example `support_queue`) and copy its data across, or, when the object's records have no user owner, declare `ownership: 'business_unit'`, `'org'` or `'none'`. The upgrade guide entry is `object-queue-field-reserved`.

Beside that, two enum additions: `ShareRecipientType` gains `queue` (a sharing rule can now name a queue as its recipient), and the `queue` approver type is authorable again (it leaves `NON_AUTHORABLE_APPROVER_TYPES`, so it is no longer marked deprecated). Two metadata types are new: `queue` and `assignment_rule`.

- **`@objectstack/spec`**: new `queue` and `assignment_rule` metadata types (`QueueSchema`, `AssignmentRuleSchema`) and the stack keys `queues` and `assignmentRules`. A declared queue's members are positions. New `IOwnershipQueueService` contract, registered under `ownership-queue`. `queue` joins the injected system columns of `ownership: 'user'` objects and `PUBLIC_FORM_SERVER_MANAGED_FIELDS`. `APPROVER_VALUE_BINDINGS.queue` reads both a declared queue and an organization's `sys_queue` row, and `APPROVER_ORG_SCOPED.queue` is `true`. The explain rule kinds `queue_membership` and `queue_claim` are new. `QUEUE_NOT_FOUND` (422) is registered under plugin-security.
- **`@objectstack/objectql`**: objects with `ownership: 'user'` get a system-managed `queue` column beside `owner_id`. Declaring your own `queue` field on one is refused at registration (`INVALID_METADATA`), and an extension's own `queue` field on one is ignored with a warning. An update can carry a write precondition: when it no longer matches, the update answers `409 CONCURRENT_UPDATE`. Declared assignment rules run once for each inserted row that has no owner and no queue after the `beforeInsert` hooks. A rule can park the row in a queue, or pick a member by `round_robin` or `least_loaded`. Assignment never refuses an insert. `kernel:ready` refuses to boot a stack that declares queues or assignment rules when no `ownership-queue` service is registered.
- **`@objectstack/metadata-core`**: `queue` is classified as an injected system column.
- **`@objectstack/metadata`**: the artifact door registers a stack's `queues` and `assignmentRules`, so an artifact-only runtime serves them.
- **`@objectstack/plugin-sharing`**: three new objects, `sys_queue`, `sys_queue_member` and `sys_assignment_cursor`, and the `ownership-queue` service. The service resolves a queue name from the registry first, then from the organization's own rows, and has one member expander. A record with no owner that sits in a queue can be read and edited by the queue's members, never deleted. The `queue` sharing recipient grants matched records to the queue's members, and its grants are recomputed when membership changes or the queue is switched on or off.
- **`@objectstack/plugin-security`**: the owner gate handles `queue`. A **claim** sets `owner_id` from empty to yourself, by id. It is admitted when the sharing model allows you the record, and only one claimant wins: the loser gets `409`. A **release** gives your own record back to its home queue. A **move** to another queue needs the transfer grant, and a queue name that does not resolve is refused with `422 QUEUE_NOT_FOUND`. An insert that names a queue needs the transfer grant or membership of that queue. A guest can supply neither. Explain reports the queue reasons on the sharing layer.
- **`@objectstack/plugin-approvals`**: a `queue` approver resolves to the queue's members when the step opens. An empty queue adds nobody and logs a warning. The `queue:<name>` placeholder is never written again.
- **`@objectstack/lint`**: a queue-only approval node is reported `approval-approvers-may-resolve-empty`, and the org-axis red line admits the queue recipient. Two new advisory rules come with `os explain` entries: `assignment-rule-user-target` (warning) and `assignment-rule-queue-unstaffed` (info).
- **`@objectstack/cli`**: the organization-ownership inventory lists `sys_queue`, `sys_queue_member` and `sys_assignment_cursor`, so `os migrate organization-ownership` plans them instead of refusing.
- **`@objectstack/runtime`**: `POST /security/_activation/:type/:name` also switches a declared `queue` or `assignment_rule` on and off.
- **`@objectstack/platform-objects`**: the two new metadata types' labels, and the queue recipient option, are translated in zh-CN, ja-JP and es-ES.
- **`@object-ui/app-shell`**: the flow designer no longer warns that a queue approver resolves to nobody.
- **`@objectstack/skills`**: the platform skill lists `queues` and `assignmentRules` among the stack's top-level keys.

What to do: rename any `queue` field on a user-owned object, or in an extension of one, as above. Every other existing stack behaves as before. To use queues, declare `queues` and `assignmentRules` in your stack, and make sure plugin-sharing is installed: it registers the `ownership-queue` service.
20 changes: 10 additions & 10 deletions content/docs/automation/approvals.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -401,28 +401,28 @@ Only `approvers` is required on the node; everything else has a default
(`behavior: 'first_response'`, `lockRecord: true`, `maxRevisions: 3`).

Approver entries resolve by kind — `position`, `user`, `field`, `manager`,
`team`, `department`, `org_membership_level`, and `expression`
`team`, `department`, `org_membership_level`, `queue`, and `expression`
(described in the [callout above](#3-the-approval-node) and in
[Dynamic approvers](#dynamic-approvers-3447)); `org_membership_level` is the
one that silently resolves to nobody when it's mistaken for a business
hierarchy. `queue` still parses so stored flows keep loading, but it is **not
implemented** by the runtime and is no longer offered for authoring (#3508) —
a queue entry resolves to nobody. Route to a `team`, `department`, or
`position` instead. `field`, `manager`, and `expression` resolve against the record's
hierarchy. A `queue` entry names an ownership queue by its machine name — one
an installed app declares, or one of the organization's own — and resolves to
the queue's members at node entry, through the same expansion that decides who
may claim the queue's records (ADR-0142 D6). `field`, `manager`, and `expression` resolve against the record's
**live** state at node entry (#3447); `manager` additionally depends on
`sys_user.manager_id`, which a platform admin sets from **Setup → Users → ⋯ →
Set Manager** — see [the `manager` callout above](#3-the-approval-node). `position`, `department`,
`org_membership_level` and `expression` may additionally name which
`org_membership_level`, `queue` and `expression` may additionally name which
organization's directory resolves them — see
[Approving across organizations](#cross-org-approvers).

What an entry that resolves to nobody leaves on the slate depends on its kind:

- **A person entry** (`manager`, `field`) adds no slot. Nor does an
`expression` that yields no ids. A `user` entry never resolves to nobody: its
value is the slot.
- **A person entry** (`manager`, `field`) adds no slot. Nor does a `queue`
with no members, or an `expression` that yields no ids. A `user` entry never
resolves to nobody: its value is the slot.
- **A group entry** (`position`, `team`, `department`, `org_membership_level`)
keeps its `<type>:<value>` literal as a slot, and so do a `queue` entry and an
keeps its `<type>:<value>` literal as a slot, and so does an
`expression` value that `resolveAs` re-expands into a group with nobody in it.
A `position:<name>` slot is decided by whoever is staffed into that position
later. No person ever holds any other literal, so only an administrator can
Expand Down
2 changes: 1 addition & 1 deletion content/docs/deployment/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,7 @@ os compile --json # JSON output for CI pipelines
→ Normalizing stack definition...
→ Lowering inline handlers...
→ Validating protocol compliance...
→ Running author-time rules (53)...
→ Running author-time rules (54)...
→ Checking that every required capability has a provider installable in this edition...
→ Collecting package docs (ADR-0046)... 0 collected
→ Writing artifact...
Expand Down
2 changes: 1 addition & 1 deletion content/docs/deployment/validating-metadata.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,7 @@ A clean run walks the registry and reports timing:
Config: /path/to/support-desk/objectstack.config.ts
Load time: 21ms
→ Validating against ObjectStack Protocol...
→ Running author-time rules (53)...
→ Running author-time rules (54)...
→ Checking that every required capability has a provider installable in this edition...
→ Checking package docs (ADR-0046)...

Expand Down
2 changes: 1 addition & 1 deletion content/docs/getting-started/build-with-claude-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ visible: 'status != "resolved"'
◆ Validate
────────────────────────────────────────
→ Validating against ObjectStack Protocol...
→ Running author-time rules (53)...
→ Running author-time rules (54)...

✗ Author-time rules failed (1 issue)
• stack · action 'resolve_ticket' visible: bare reference `status` — a
Expand Down
3 changes: 2 additions & 1 deletion content/docs/references/api/automation-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ const result = AutomationApiErrorCode.parse(data);

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +346 more>` | ✅ | The inner refusal's code in its WIRE spelling - the ADR-0112 member a record save of the same write answers in `error.code` (e.g. `UNIQUE_VIOLATION`); the closed set `ApiErrorSchema.code` parses against |
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +347 more>` | ✅ | The inner refusal's code in its WIRE spelling - the ADR-0112 member a record save of the same write answers in `error.code` (e.g. `UNIQUE_VIOLATION`); the closed set `ApiErrorSchema.code` parses against |
| **message** | `string` | ✅ | The inner refusal's own diagnostic text, verbatim, without the node and operation prefixes the run's failure text adds |

### Allowed Values: `FlowInnerRefusal.code`
Expand Down Expand Up @@ -615,6 +615,7 @@ const result = AutomationApiErrorCode.parse(data);
* `PROJECT_PROVISIONING`
* `PROJECT_PROVISIONING_FAILED`
* `QUERY_OBJECT_MISMATCH`
* `QUEUE_NOT_FOUND`
* `RAW_SQL_UNSUPPORTED`
* `READ_BACK_FAILED`
* `READ_SCOPE_COMPILE_FAILED`
Expand Down
3 changes: 2 additions & 1 deletion content/docs/references/api/contract.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const result = ApiErrorSchema.parse(data);

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +346 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +347 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112) |
| **message** | `string` | ✅ | Readable error message |
| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces `message`. |
Expand Down Expand Up @@ -303,6 +303,7 @@ const result = ApiErrorSchema.parse(data);
* `PROJECT_PROVISIONING`
* `PROJECT_PROVISIONING_FAILED`
* `QUERY_OBJECT_MISMATCH`
* `QUEUE_NOT_FOUND`
* `RAW_SQL_UNSUPPORTED`
* `READ_BACK_FAILED`
* `READ_SCOPE_COMPILE_FAILED`
Expand Down
1 change: 1 addition & 0 deletions content/docs/references/api/error-code-ledger.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ const result = ErrorCode.parse(data);
* `PROJECT_PROVISIONING`
* `PROJECT_PROVISIONING_FAILED`
* `QUERY_OBJECT_MISMATCH`
* `QUEUE_NOT_FOUND`
* `RAW_SQL_UNSUPPORTED`
* `READ_BACK_FAILED`
* `READ_SCOPE_COMPILE_FAILED`
Expand Down
6 changes: 4 additions & 2 deletions content/docs/references/api/metadata.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@ Metadata query with filtering, sorting, and pagination

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **types** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'picklist' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'api' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'position' \| 'capability' \| 'sharing_rule' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types |
| **types** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'picklist' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'api' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'position' \| 'capability' \| 'sharing_rule' \| 'queue' \| 'assignment_rule' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types |
| **namespaces** | `string[]` | optional | Filter by namespaces |
| **packageId** | `string` | optional | Filter by owning package |
| **search** | `string` | optional | Full-text search query |
Expand Down Expand Up @@ -707,7 +707,7 @@ Metadata query with filtering, sorting, and pagination

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **type** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'picklist' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| … +14 more>` | ✅ | Metadata type |
| **type** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'picklist' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| … +16 more>` | ✅ | Metadata type |
| **name** | `string` | ✅ | Item name (snake_case) |
| **data** | `Record<string, any>` | ✅ | Metadata payload |
| **namespace** | `string` | optional | Optional namespace |
Expand Down Expand Up @@ -740,6 +740,8 @@ Metadata query with filtering, sorting, and pagination
* `position`
* `capability`
* `sharing_rule`
* `queue`
* `assignment_rule`
* `agent`
* `tool`
* `skill`
Expand Down
4 changes: 4 additions & 0 deletions content/docs/references/api/package-api-assembled.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ Installed package row whose manifest is the assembled package body
| **books** | `{ name: string; label?: string; description?: string; slug?: string; … }[]` | optional | Documentation navigation spines — ordered groups with derived membership (ADR-0046 §6) |
| **positions** | `{ name: string; label: string; description?: string; permissionSets?: string[]; … }[]` | optional | Positions — flat capability-distribution groups (ADR-0090 D3) |
| **sharingRules** | `{ name: string; label?: string; description?: string; object: string; … }[]` | optional | Record Sharing Rules |
| **queues** | `{ name: string; label: string; description?: string; members?: object[]; … }[]` | optional | [ADR-0142 D1] Ownership queues — named sets of people records can sit in until a member claims them |
| **assignmentRules** | `{ name: string; label?: string; description?: string; object: string; … }[]` | optional | [ADR-0142 D5] Assignment rules — route new ownerless records to a queue or a person, once, on insert |
| **apis** | `{ name: string; path: string; method: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>; summary?: string; … }[]` | optional | API Endpoints — declared endpoints are live from protocol 17; each is gated at publish (ADR-0121) |
| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete' \| 'bulk_update' \| 'bulk_delete'>[]; … }[]` | optional | Outbound Webhooks |
| **agents** | `{ name: string; label: string; avatar?: string; role: string; … }[]` | optional | AI Agents — platform-internal (ADR-0063 §2): the kernel ships exactly two (ask/build); third parties extend via skills, not agents |
Expand Down Expand Up @@ -374,6 +376,8 @@ This schema accepts one of the following structures:
| **books** | `{ name: string; label?: string; description?: string; slug?: string; … }[]` | optional | Documentation navigation spines — ordered groups with derived membership (ADR-0046 §6) |
| **positions** | `{ name: string; label: string; description?: string; permissionSets?: string[]; … }[]` | optional | Positions — flat capability-distribution groups (ADR-0090 D3) |
| **sharingRules** | `{ name: string; label?: string; description?: string; object: string; … }[]` | optional | Record Sharing Rules |
| **queues** | `{ name: string; label: string; description?: string; members?: object[]; … }[]` | optional | [ADR-0142 D1] Ownership queues — named sets of people records can sit in until a member claims them |
| **assignmentRules** | `{ name: string; label?: string; description?: string; object: string; … }[]` | optional | [ADR-0142 D5] Assignment rules — route new ownerless records to a queue or a person, once, on insert |
| **apis** | `{ name: string; path: string; method: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>; summary?: string; … }[]` | optional | API Endpoints — declared endpoints are live from protocol 17; each is gated at publish (ADR-0121) |
| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete' \| 'bulk_update' \| 'bulk_delete'>[]; … }[]` | optional | Outbound Webhooks |
| **agents** | `{ name: string; label: string; avatar?: string; role: string; … }[]` | optional | AI Agents — platform-internal (ADR-0063 §2): the kernel ships exactly two (ask/build); third parties extend via skills, not agents |
Expand Down
Loading
Loading