Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 27 additions & 2 deletions content/docs/automation/approvals.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ This is **not** a schedule-only concern, and for approvals it is the common case

### 3. The approval node

The node declares **who approves** (a named user, a position, the submitter's manager, …) and what happens on approve / reject / escalate. The authoritative shape is [`ApprovalNodeConfig` / `ApproverType`](/docs/references/automation/approval); a flow strings the trigger, the approval node, and the post-decision branches together.
The node declares **who approves** (a named user, a position, the submitter's manager, …) and what happens on approve / reject / escalate. Not every kind is configured the same way: the submitter's manager resolves from a column no product surface writes, so read the `manager` callout below before you reach for it. The authoritative shape is [`ApprovalNodeConfig` / `ApproverType`](/docs/references/automation/approval); a flow strings the trigger, the approval node, and the post-decision branches together.

```ts
// Illustrative — see the Approval reference for the exact node schema.
Expand Down Expand Up @@ -63,6 +63,29 @@ Authored `type: 'role'` on 15.x? That is the deprecated spelling of `org_members
(ADR-0090 D3): it still resolves, warns at runtime, and is removed in the next major.
</Callout>

<Callout type="warn">
**`manager` is a directory-sync dependency, not something you configure here.**
`{ type: 'manager' }` resolves at runtime from `sys_user.manager_id` on the record's owner, and
that column is not one an operator can fill in from inside the product.

`sys_user.manager_id` has no product write surface — the data API's managed-update whitelist is
`{name, image, locale}`, the auth admin endpoints do not accept the column and the Console
renders no field for it, so it is never populated by editing the user in the Console. On this
platform the column is written by a seed, or by any other system-context write, which bypasses
the managed-update whitelist. SCIM provisioning and directory sync can populate it too, but only
through a provisioning path your own deployment supplies: this platform declares the SCIM
`manager` attribute without projecting it onto the column, and its admin bulk import does not
write it either.

So where `manager_id` is unset the slate resolves to nobody: the request opens with an empty
`pending_approvers` and waits forever, and under the default `lockRecord: true` the record stays
locked with no in-product recovery. Populate the column for everyone who submits this request,
or add a fallback approver that cannot resolve empty — e.g.
`{ type: 'org_membership_level', value: 'owner' }`. `os lint` reports this at authoring time
(`approval-approvers-may-resolve-empty`, `info`) and carries the same remedy text, so the two
cannot drift apart.
</Callout>

Approving is itself a gated action — model "may approve" as a capability (`approve_invoice`) the approver's permission set grants, and gate the approve action's `requiredPermissions` on it so the gate is enforced on **both** the UI and the server (ADR-0066 D4).

## Approval nodes in practice
Expand Down Expand Up @@ -303,7 +326,9 @@ 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
**live** state at node entry (#3447). `position`, `department`,
**live** state at node entry (#3447); `manager` additionally depends on
`sys_user.manager_id`, which no product surface writes — see [the `manager`
callout above](#3-the-approval-node). `position`, `department`,
`org_membership_level` and `expression` may additionally name which
organization's directory resolves them — see
[Approving across organizations](#cross-org-approvers). An entry that resolves
Expand Down
13 changes: 12 additions & 1 deletion content/docs/capabilities/approvals.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,18 @@ Discounts, contracts, spend — anything that needs a human "yes" becomes an app

## Who approves — eight resolution styles

A specific person · everyone holding a position (e.g. sales managers) · the submitter's direct manager · a team · a department (optionally including sub-departments) · organization leadership · **a person named on the record itself** (e.g. the "regional director" field) · a shared queue (first responder claims it).
A specific person · everyone holding a position (e.g. sales managers) · the submitter's direct manager (needs a provisioned reporting chain — see below) · a team · a department (optionally including sub-departments) · organization leadership · **a person named on the record itself** (e.g. the "regional director" field) · a shared queue (first responder claims it).

<Callout type="warn">
**Routing to the submitter's manager needs a reporting chain you provide.** Unlike a position you
staff, a team you add people to, or a field you name on the record, this style does not get its
data from inside the product: it resolves `sys_user.manager_id`, and no Console field, data-API
write or auth admin endpoint sets that column. Your own provisioning fills it — a seed or other
system-context write, or a SCIM / directory-sync path your deployment supplies. Until it is
filled, a manager-only approval step resolves to nobody and the request waits. The exact remedy,
and the fallback that needs no provisioning at all, are in
[Automation → Approvals](/docs/automation/approvals).
</Callout>

## How it passes — four decision modes

Expand Down
Loading