You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit d7b2170
Browse filesBrowse the repository at this point in the historyBrowse files
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>
Copy file name to clipboardExpand all lines: content/docs/api/declarative-endpoints.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -101,7 +101,7 @@ declaration to shadow a built-in one. Match → policy chain (`rateLimit` → `a
101
101
|`type`| Delegates to | Request shape |
102
102
|:---|:---|:---|
103
103
|`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|
105
105
106
106
The frozen vocabulary has **no path-template syntax**, so an endpoint cannot express
107
107
`/leads/{id}` — a record id travels as `?id=…`. A method that no declaration claims
Copy file name to clipboardExpand all lines: content/docs/automation/flows.mdx
+7-5Lines changed: 7 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2057,18 +2057,20 @@ curl -b cookies.txt -X POST \
2057
2057
|`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 |
2058
2058
2059
2059
**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:
2062
2063
2063
2064
| Status |`error.code`| What happened | What to do |
2064
2065
|:---|:---|:---|:---|
2065
2066
|`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) |
2066
2068
|`409`|`FLOW_DISABLED`| The flow exists but is switched off | Enable it — the identical request then succeeds |
2067
2069
|`422`|`FLOW_NO_START_NODE`| The stored definition has no `start` node | Fix the flow; retrying cannot help |
2068
2070
|`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 |
2069
2071
|`400`|`FLOW_FAILED`| The flow **ran** and was rejected | Read `error.details.summary` for the failing node |
2070
2072
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
2072
2074
written, and there is no run to look up. Only `400` describes a run. The
2073
2075
`FLOW_INPUT_SCHEMA_INVALID` verdict is a pure function of the flow definition —
2074
2076
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.
2079
2081
`POST /api/v1/actions/:object/:action`, and calling a declared `type: 'flow'`
2080
2082
[endpoint](/docs/protocol/kernel/http-protocol) under `/api/v1/apps/`, both
2081
2083
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
Copy file name to clipboardExpand all lines: content/docs/ui/actions.mdx
+5-3Lines changed: 5 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -407,8 +407,10 @@ is a spec proposal for a properly named key, not a values map under this one.
407
407
-**`requiredPermissions: ['can_close_tickets']`** is a **dual-surface gate**
408
408
(one declaration, two enforcement points): the server rejects unauthorized
409
409
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.
412
414
-**`visible`** is a CEL predicate evaluated **fail-closed**: an expression
413
415
that throws hides the action silently. Two rules save real debugging time:
414
416
@@ -544,7 +546,7 @@ The endpoint dispatches on the **declared `type`**, exactly like the MCP
544
546
|`type`| Over REST|
545
547
|:---|:---|
546
548
|`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. |
548
550
| `api` | **400** — it dispatches on `target`; call that endpoint directly. |
549
551
| `url` / `modal` / `form` | **400** — client-side navigation; there is nothing for the server to run. |
0 commit comments