diff --git a/docs/reference.md b/docs/reference.md index 48d8eae..0cbb592 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -26,6 +26,7 @@ The quick lookup surface: one line and a minimal snippet per construct. For rule | [`relations` / `composition`](/spec/relations#relations) | associations and master-detail compositions | | [`uses`](/spec/relations#multi-model-applications) | reuse entities owned by another intent model | | [`processes`](/spec/processes#processes) | workflows: user tasks, decisions, waits, boundary timers | +| [task assignment](/spec/processes#task-assignment) | route a user task to a role, the record owner, or a relation walk | | [`abortOn`](/spec/processes#aborton-cancel-the-instance-on-a-terminal-status) | cancel the running instance when the document reaches a terminal status | | [`function: Attachment` / `Snapshot`](/spec/entities#attachments-and-snapshots) | a Files panel / immutable versioned printed copies | | [`forms`](/spec/processes#forms) | task data-entry pages | @@ -164,4 +165,3 @@ The following are parsed (or reserved) but not yet materialised by a generator; - **Cross-model status names and stage scopes** — a nomenclature owned by another model is seeded there, so its stages and names cannot be resolved from the referencing file; such references are rejected with the numeric-id fallback named. - **`manyToMany`** — parsed but never materialised; the supported shape is the [explicit intermediate entity](/spec/relations#many-to-many). - Event-driven document generation (produce a document on an event), a declarative state machine, and shadow audit-history entities (audit *columns* via `audit: true` ship today). -- Arbitrary resolver-path task assignment beyond `assignee: personal`. diff --git a/docs/spec/processes.md b/docs/spec/processes.md index 2ae402e..b0cec9f 100644 --- a/docs/spec/processes.md +++ b/docs/spec/processes.md @@ -145,7 +145,19 @@ trigger: { onCreate: Order, businessKey: orderNo, businessKeyStrategy: timestamp ### Task assignment -A user task's `assignee` is a role / candidate-group name, or the literal **`assignee: personal`** to route the task to the **record owner's** inbox (requires the trigger entity to declare a `personal:` relation — see [scoped surfaces](/spec/surfaces)). +A user task's `assignee` is a role / candidate-group name, or the literal **`assignee: personal`** to route the task to the **record owner's** inbox (requires the trigger entity to declare a `personal:` relation — see [scoped surfaces](/spec/surfaces)), or a **relation walk** off the trigger record: + +```yaml +- name: approve + kind: userTask + args: + assignee: { path: employee.manager, fallback: manager } + form: ApproveRequest +``` + +Every segment of `path` is a **to-one relation** — the first of the trigger entity, each further one of the previous target — and the walk ends at an entity that declares `identity`, which is what maps a record to a login. A **cross-model** relation may only be the **last** segment: a projection carries the target's own properties but not its relations, so there is nothing to walk on from there. A conforming generator validates every hop when the file is read, so a dangling segment is reported then rather than when the process runs. + +`fallback` is **required** and names the candidate group. The walk is resolved when the task is reached, not when the process starts — so a relation an earlier step of the same process set is visible — and when it resolves to nobody (a null hop, a missing record, a blank identity) the task is created **unassigned** and the fallback group can still claim it. That is what makes the unresolvable case total: a resolver path can never mint a task nobody can see. ## forms @@ -175,4 +187,4 @@ actions: - [Declarative glue](/spec/glue) — event-driven glue (`wait`, `timeout`, triggers) is generated as integration code alongside the process. - [Presentation](/spec/presentation) — the document view a process's status pill and inline task list appear on. -- [Scoped surfaces & roles](/spec/surfaces) — `assignee: personal` and the roles a candidate group maps to. +- [Scoped surfaces & roles](/spec/surfaces) — `assignee: personal`, the `identity` a walk ends at, and the roles a candidate group maps to. diff --git a/docs/spec/surfaces.md b/docs/spec/surfaces.md index 1e4e2de..a9a6f5c 100644 --- a/docs/spec/surfaces.md +++ b/docs/spec/surfaces.md @@ -31,7 +31,7 @@ entities: A scope's safety is by **construction, not by a filter**: the scoped controller only ever queries the caller's own rows, and a sensitive field is on an allow-list the scoped serialiser never includes. A field hidden only in the UI is cosmetic; `sensitive` is a server-side guarantee. ::: -A user task can also be routed to the record owner's inbox with the literal `assignee: personal`, which resolves the owner through the `personal:` relation (see [processes](/spec/processes#task-assignment)). +A user task can also be routed to the record owner's inbox with the literal `assignee: personal`, which resolves the owner through the `personal:` relation, or to whoever a relation walk off the record names — `assignee: { path, fallback }`, whose walk likewise ends at an `identity`-declaring entity (see [processes](/spec/processes#task-assignment)). ## permissions @@ -46,5 +46,5 @@ Generates a deduplicated set of **roles**. It deliberately does **not** emit URL ## See also - [Entities & fields](/spec/entities) — `sensitive` as a field attribute, and the entity a scope attaches to. -- [Processes & forms](/spec/processes) — `assignee: personal` task routing. +- [Processes & forms](/spec/processes) — `assignee: personal` and resolver-path task routing. - [Relations & multi-model](/spec/relations) — the to-one relation a scope is declared on.