Skip to content

Commit d7b2170

Browse files
committed
docs(automation,ui,api): the flow-dispatch tables list the 403 every door answers an elevated self-triggered start
The status table on the flows page gains the 403 PERMISSION_DENIED row for a non-system caller starting a runAs-system flow of a self-triggered type, the same at the trigger route, a flow action and a declared flow endpoint. Its lead sentence now says that row is the door's own admission check, not the engine's verdict, and the never-dispatched count moves from four to five. The actions page's flow row and the requiredPermissions bullet, and the declarative endpoints page's flow row, state the same refusal. Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
1 parent 28fe107 commit d7b2170

3 files changed

Lines changed: 13 additions & 9 deletions

File tree

‎content/docs/api/declarative-endpoints.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ declaration to shadow a built-in one. Match → policy chain (`rateLimit` → `a
101101
| `type` | Delegates to | Request shape |
102102
|:---|:---|:---|
103103
| `object_operation` | the same `callData` binding that serves `/api/v1/data/{object}` | `find` reads its criteria from the query string; `get` / `update` / `delete` take the record id from `query.id`; `create` / `update` take the body. `create` answers `201`, the rest `200` |
104-
| `flow` | the same automation pipeline as `POST /api/v1/automation/{name}/trigger` | the request body is the flow input. A refused or failed run answers a real status: `404` unknown flow, `409` `FLOW_DISABLED`, `422` `FLOW_NO_START_NODE`, `422` `FLOW_INPUT_SCHEMA_INVALID`, `400` `FLOW_FAILED` |
104+
| `flow` | the same automation pipeline as `POST /api/v1/automation/{name}/trigger` | the request body is the flow input. A refused or failed run answers a real status: `404` unknown flow, `409` `FLOW_DISABLED`, `422` `FLOW_NO_START_NODE`, `422` `FLOW_INPUT_SCHEMA_INVALID`, `400` `FLOW_FAILED`, and `403` `PERMISSION_DENIED`, nothing run, when the target is declared `runAs: 'system'` with a self-triggered type (`autolaunched`, `record_change`, `schedule`), exactly as the trigger door answers |
105105

106106
The frozen vocabulary has **no path-template syntax**, so an endpoint cannot express
107107
`/leads/{id}` — a record id travels as `?id=…`. A method that no declaration claims

‎content/docs/automation/flows.mdx‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2057,18 +2057,20 @@ curl -b cookies.txt -X POST \
20572057
| `POST /api/v1/automation/:name/runs/:runId/restore-suspension` | Re-arm a run stranded by a failed resume — and, for a nested run, every ancestor whose pause that strand's cascade consumed, deepest first, from whichever member you name. Same gate as `cancel`; body `{ reason? }`. `501 NOT_IMPLEMENTED` when the service does not declare the verb |
20582058

20592059
**Failures answer real HTTP status codes.** A flow that does not run is not a
2060-
`200`. The route classifies five outcomes, and it reads the engine's own verdict
2061-
to do it — never the message text:
2060+
`200`. The route classifies six outcomes. Five of them it reads off the engine's
2061+
own verdict — never the message text; the `403` is the door's own admission
2062+
check, made before anything is dispatched:
20622063

20632064
| Status | `error.code` | What happened | What to do |
20642065
|:---|:---|:---|:---|
20652066
| `404` | — | No flow by that name in this deployment | Check the name; nothing was dispatched |
2067+
| `403` | `PERMISSION_DENIED` | The caller is not the system principal, and the flow is declared `runAs: 'system'` with a self-triggered `type` (`autolaunched`, `record_change`, `schedule`). The same answer at every door that starts a flow by name: this route, a `type: 'flow'` action (REST and MCP `run_action`) and a declared `type: 'flow'` endpoint | Let it run on its own trigger, or call it from a parent flow's `subflow` node. If members are meant to start it, declare it `type: 'screen'` (or `'api'` for a signed inbound hook) |
20662068
| `409` | `FLOW_DISABLED` | The flow exists but is switched off | Enable it — the identical request then succeeds |
20672069
| `422` | `FLOW_NO_START_NODE` | The stored definition has no `start` node | Fix the flow; retrying cannot help |
20682070
| `422` | `FLOW_INPUT_SCHEMA_INVALID` | A node's `config` violates the `inputSchema` its own definition declares | Fix the flow; the refusal is non-retryable — even under `errorHandling.strategy: 'retry'` the engine refuses once and never enters the retry loop |
20692071
| `400` | `FLOW_FAILED` | The flow **ran** and was rejected | Read `error.details.summary` for the failing node |
20702072

2071-
The first four never dispatched anything: no node executed, no record was
2073+
The first five never dispatched anything: no node executed, no record was
20722074
written, and there is no run to look up. Only `400` describes a run. The
20732075
`FLOW_INPUT_SCHEMA_INVALID` verdict is a pure function of the flow definition —
20742076
re-running the guard cannot change the answer — so a `retry` strategy does not
@@ -2079,8 +2081,8 @@ burn its budget on it: the run log holds exactly one refusal row.
20792081
`POST /api/v1/actions/:object/:action`, and calling a declared `type: 'flow'`
20802082
[endpoint](/docs/protocol/kernel/http-protocol) under `/api/v1/apps/`, both
20812083
dispatch the same flow through the same service call and classify the outcome
2082-
the same way — the table above has one definition that all three doors read, so
2083-
they cannot answer one engine outcome differently. Branch on the status and
2084+
the same way — each row of the table above has one definition that all three
2085+
doors read, so they cannot answer one request differently. Branch on the status and
20842086
`error.code` at any of them.
20852087

20862088
```

‎content/docs/ui/actions.mdx‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -407,8 +407,10 @@ is a spec proposal for a properly named key, not a values map under this one.
407407
- **`requiredPermissions: ['can_close_tickets']`** is a **dual-surface gate**
408408
(one declaration, two enforcement points): the server rejects unauthorized
409409
calls with 403, and the UI hides or disables the button for the same users.
410-
Unset means no gate beyond object CRUD permissions. Referenced capabilities
411-
must exist — `os lint` checks that.
410+
Unset means no gate beyond object CRUD permissions — except that a `flow`
411+
action whose target is an elevated self-triggered flow is refused to every
412+
caller but the system principal either way (the `flow` row below). Referenced
413+
capabilities must exist — `os lint` checks that.
412414
- **`visible`** is a CEL predicate evaluated **fail-closed**: an expression
413415
that throws hides the action silently. Two rules save real debugging time:
414416

@@ -544,7 +546,7 @@ The endpoint dispatches on the **declared `type`**, exactly like the MCP
544546
| `type` | Over REST |
545547
|:---|:---|
546548
| `script` | Runs the registered handler / inline body. |
547-
| `flow` | Runs `target` on the automation engine, with your identity forwarded (a `runAs: 'user'` flow enforces RLS as you). Dispatches the same flow as `POST /api/v1/automation/:target/trigger`, without having to know the flow name — **and answers the same way**: a run that ran and was rejected is **400** `FLOW_FAILED`, while a dispatch that never happened is separated out (**404** unknown flow / **409** `FLOW_DISABLED` / **422** `FLOW_NO_START_NODE` / **422** `FLOW_INPUT_SCHEMA_INVALID`). See [Run a flow via API](/docs/automation/flows#run-a-flow-via-api) for the full table — it is one table, read by both doors. |
549+
| `flow` | Runs `target` on the automation engine, with your identity forwarded (a `runAs: 'user'` flow enforces RLS as you). Dispatches the same flow as `POST /api/v1/automation/:target/trigger`, without having to know the flow name — **and answers the same way**: a run that ran and was rejected is **400** `FLOW_FAILED`, while a dispatch that never happened is separated out (**404** unknown flow / **409** `FLOW_DISABLED` / **422** `FLOW_NO_START_NODE` / **422** `FLOW_INPUT_SCHEMA_INVALID`). A target declared `runAs: 'system'` whose type is self-triggered (`autolaunched`, `record_change`, `schedule`) is refused **403** `PERMISSION_DENIED` to every caller but the system principal, exactly as at the trigger door, and nothing runs; holding the action's `requiredPermissions` does not open it. See [Run a flow via API](/docs/automation/flows#run-a-flow-via-api) for the full table — it is one table, read by both doors. |
548550
| `api` | **400** — it dispatches on `target`; call that endpoint directly. |
549551
| `url` / `modal` / `form` | **400** — client-side navigation; there is nothing for the server to run. |
550552

0 commit comments

Comments
 (0)