Defines REST CRUD endpoint schemas for managing automation flows, triggering executions, and querying execution history.
Automation API Protocol
Defines REST CRUD endpoint schemas for managing automation flows,
triggering executions, and querying execution history.
Base path: /api/v1/automation
The wire paths the platform serves: the dispatcher mounts this door at its
prefix (default /api/v1, the one objectstack serve uses) plus
/automation. A drift pin in @objectstack/runtime
(automation-api-contract-mounts.test.ts) holds every path in
AutomationApiContracts to that mount table.
The flow LIST is not on this door. Flows are metadata (ADR-0106), and the
governed read of them is GET /api/v1/meta/flow (client.meta.getItems);
the former GET /api/v1/automation list route, its request/response schemas
and client.automation.list are retired (ADR-0087 semantic entry
automation-flow-list-route-retired).
The toggle door switches PACKAGED flows only: a flow a code package ships.
It records the installation's choice in the packaged-metadata activation
ledger (ADR-0126 §7.2). A flow authored in the deployment is not switched
there. Its switch is its own status: 'obsolete' disarms it and
'active' arms it, published with the complete definition through
PUT /api/v1/automation/:name. The toggle door refuses such a flow with
409 RESOURCE_CONFLICT, names that switch, and changes nothing.
Endpoints
GET /api/v1/automation/:name — Get flowPOST /api/v1/automation — Create flowPUT /api/v1/automation/:name — Update flowDELETE /api/v1/automation/:name — Delete flowPOST /api/v1/automation/:name/trigger — Trigger flow executionPOST /api/v1/automation/:name/toggle — Enable/disable a packaged flowGET /api/v1/automation/:name/runs — List execution runsGET /api/v1/automation/:name/runs/:runId — Get single execution run
Message carried on AutomationResult for every terminal run (not only screen flows); the screen-flow UI shows it as a toast instead of a generic "Done".
errorMessage
string
optional
Message carried on AutomationResult for every terminal run (not only screen flows); the screen-flow UI shows it as a toast instead of the raw error.
version
integer
optional (default: 1)
Version number
status
Enum<'draft' | 'active' | 'obsolete' | 'invalid'>
optional (default: "draft")
Deployment status
template
never
optional
[REMOVED] flow.template was removed in @objectstack/spec 17.0.0 (audit close-out) — no designer or engine path ever read it, so flagging a flow as a template/subflow did nothing. Delete the key. Shared logic is invoked via a subflow NODE referencing the flow by name. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
[REMOVED] flow.active was removed in @objectstack/spec 17.0.0 (audit close-out) — it never had an effect: the engine arms flows from status, and active: false did NOT stop a flow (worse, the default read as disabled while the engine treated unset as enabled). Delete the key. Use status: 'obsolete' (or 'invalid') to unbind and disable a flow, status: 'active' to arm it. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
runAs
Enum<'system' | 'user'>
optional (default: "user")
Execution identity for the run: system = elevated (bypasses RLS), user = the triggering user (RLS-respecting). A run with no trigger user has no identity to scope to, so under user its data operations are REFUSED — declare system to make the elevation explicit. This covers schedule/time-relative/api triggers AND any record-change flow fired by a write that carried no user.
Flow-level error handling configuration. A durable pause ends the retry-governed segment: strategy: 'retry' describes one synchronous dispatch, so a run that parks on an approval/screen/wait node and later resumes gets one attempt for anything that fails after the pause. Protect the post-pause half with its own failure handling in the flow — a try_catch node's retry around the post-resume work, or fault edges to a handler node.
Value bound at run start when no parameter supplies one — this is what makes a declared variable always bound. An explicitly supplied param wins, including false and null; the boundary is params[name] !== undefined.
Action type — a built-in FlowNodeAction id or a plugin-registered node type. Validated against the live action registry at registerFlow() (ADR-0018), not by a closed enum.
[REMOVED] flow.nodes[].outputSchema was removed in @objectstack/spec 17.0.0 (audit close-out) — it was never validated: the engine does not check node outputs against it, so it documented a contract nothing enforced. Delete the key. Downstream nodes read prior outputs via expressions ({{nodeId.field}}) regardless of any declaration. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
Configuration for wait node event resumption. REQUIRED on a type: 'wait' node — the block is the whole contract, and a wait node without one parses into a run that parks forever reporting success. Under eventType: 'timer' a timerDuration is required too.
Configuration for boundary events attached to host nodes. REQUIRED on a type: 'boundary_event' node — without it the node names neither the activity it watches nor what fires it.
Predicate (CEL) returning boolean used for branching. An evaluated slot: a bare non-blank CEL string, or an envelope carrying a non-blank source — an ast-only envelope, and a source that is blank after trimming, are refused at authoring because the engine evaluates source alone and would otherwise answer a silent false.
Connection type: default (normal flow), fault (error path), conditional (expression-guarded), or back (ADR-0044 declared back-edge — traversed normally at run time, but excluded from DAG cycle validation so a revise/rework loop can re-enter an earlier node)
label
string
optional
Label on the connector
isDefault
boolean
optional (default: false)
BPMN default flow: traverse this edge only when no sibling conditional edge of the same source node matched. Mutually exclusive with condition; at most one per source node.
How to handle node execution errors. 'retry' governs ONE synchronous dispatch: a durable pause (approval/screen/wait) ends the retry-governed segment, so a failure after the run resumes is not retried.
maxRetries
integer
optional (default: 0)
Retry attempts after the initial one. Read only under strategy: 'retry', which requires >= 1; 0 (the default) means no retry.
backoffMs
integer
optional (default: 1000)
Base delay before the first retry (ms); subsequent delays multiply by backoffMultiplier
backoffMultiplier
number
optional (default: 1)
Exponential backoff multiplier; 1 (the default) keeps the delay flat
maxRetryDelayMs
integer
optional (default: 30000)
Ceiling for a single backoff delay (ms)
jitter
boolean
optional (default: false)
Randomize each delay within [50%, 100%] of its computed value — spreads a thundering herd of simultaneous retries
retryDelayMs
never
optional
[REMOVED] retryDelayMs was removed in @objectstack/spec 17.0.0 — the retry policy now has ONE spelling for its base delay across every surface that carries it: job.retryPolicy, a try_catch node's retry and flow.errorHandling. Rename the key to backoffMs; the value (milliseconds before the first retry) is unchanged. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
fallbackNodeId
never
optional
[REMOVED] flow.errorHandling.fallbackNodeId was removed in @objectstack/spec 17.0.0 (audit close-out) — the engine routes unrecoverable node errors via per-node fault edges (an edge with type: 'fault'), and never read this key: a fallback configured here silently did not exist. Delete the key and draw a fault edge from the failing node to the handler node instead. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
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.
refusal
true
optional
Producer-declared: the 5xx this envelope carries is a deliberate refusal whose message is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose message is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — true is the only value.
category
string
optional
Error category (e.g. validation, authorization)
httpStatus
integer
optional
HTTP status of the response carrying this error
details
any
optional
Additional error context (e.g. field validation errors)
Message carried on AutomationResult for every terminal run (not only screen flows); the screen-flow UI shows it as a toast instead of a generic "Done".
errorMessage
string
optional
Message carried on AutomationResult for every terminal run (not only screen flows); the screen-flow UI shows it as a toast instead of the raw error.
version
integer
optional (default: 1)
Version number
status
Enum<'draft' | 'active' | 'obsolete' | 'invalid'>
optional (default: "draft")
Deployment status
template
never
optional
[REMOVED] flow.template was removed in @objectstack/spec 17.0.0 (audit close-out) — no designer or engine path ever read it, so flagging a flow as a template/subflow did nothing. Delete the key. Shared logic is invoked via a subflow NODE referencing the flow by name. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
[REMOVED] flow.active was removed in @objectstack/spec 17.0.0 (audit close-out) — it never had an effect: the engine arms flows from status, and active: false did NOT stop a flow (worse, the default read as disabled while the engine treated unset as enabled). Delete the key. Use status: 'obsolete' (or 'invalid') to unbind and disable a flow, status: 'active' to arm it. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
runAs
Enum<'system' | 'user'>
optional (default: "user")
Execution identity for the run: system = elevated (bypasses RLS), user = the triggering user (RLS-respecting). A run with no trigger user has no identity to scope to, so under user its data operations are REFUSED — declare system to make the elevation explicit. This covers schedule/time-relative/api triggers AND any record-change flow fired by a write that carried no user.
Flow-level error handling configuration. A durable pause ends the retry-governed segment: strategy: 'retry' describes one synchronous dispatch, so a run that parks on an approval/screen/wait node and later resumes gets one attempt for anything that fails after the pause. Protect the post-pause half with its own failure handling in the flow — a try_catch node's retry around the post-resume work, or fault edges to a handler node.
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.
refusal
true
optional
Producer-declared: the 5xx this envelope carries is a deliberate refusal whose message is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose message is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — true is the only value.
category
string
optional
Error category (e.g. validation, authorization)
httpStatus
integer
optional
HTTP status of the response carrying this error
details
any
optional
Additional error context (e.g. field validation errors)
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.
refusal
true
optional
Producer-declared: the 5xx this envelope carries is a deliberate refusal whose message is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose message is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — true is the only value.
category
string
optional
Error category (e.g. validation, authorization)
httpStatus
integer
optional
HTTP status of the response carrying this error
details
any
optional
Additional error context (e.g. field validation errors)
Message carried on AutomationResult for every terminal run (not only screen flows); the screen-flow UI shows it as a toast instead of a generic "Done".
errorMessage
string
optional
Message carried on AutomationResult for every terminal run (not only screen flows); the screen-flow UI shows it as a toast instead of the raw error.
version
integer
optional (default: 1)
Version number
status
Enum<'draft' | 'active' | 'obsolete' | 'invalid'>
optional (default: "draft")
Deployment status
template
never
optional
[REMOVED] flow.template was removed in @objectstack/spec 17.0.0 (audit close-out) — no designer or engine path ever read it, so flagging a flow as a template/subflow did nothing. Delete the key. Shared logic is invoked via a subflow NODE referencing the flow by name. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
[REMOVED] flow.active was removed in @objectstack/spec 17.0.0 (audit close-out) — it never had an effect: the engine arms flows from status, and active: false did NOT stop a flow (worse, the default read as disabled while the engine treated unset as enabled). Delete the key. Use status: 'obsolete' (or 'invalid') to unbind and disable a flow, status: 'active' to arm it. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
runAs
Enum<'system' | 'user'>
optional (default: "user")
Execution identity for the run: system = elevated (bypasses RLS), user = the triggering user (RLS-respecting). A run with no trigger user has no identity to scope to, so under user its data operations are REFUSED — declare system to make the elevation explicit. This covers schedule/time-relative/api triggers AND any record-change flow fired by a write that carried no user.
Flow-level error handling configuration. A durable pause ends the retry-governed segment: strategy: 'retry' describes one synchronous dispatch, so a run that parks on an approval/screen/wait node and later resumes gets one attempt for anything that fails after the pause. Protect the post-pause half with its own failure handling in the flow — a try_catch node's retry around the post-resume work, or fault edges to a handler node.
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.
refusal
true
optional
Producer-declared: the 5xx this envelope carries is a deliberate refusal whose message is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose message is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — true is the only value.
category
string
optional
Error category (e.g. validation, authorization)
httpStatus
integer
optional
HTTP status of the response carrying this error
details
any
optional
Additional error context (e.g. field validation errors)
[REMOVED] cursor was removed from GET /api/v1/automation/:name/runs in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it was VALIDATED at the boundary and then read by nothing: the option reached the service and the engine never looked at it, no emit site has ever written the response half nextCursor, and the only ordering this door has is a required but non-unique startedAt timestamp that nothing ever minted a resume point from — so a caller looping "until the cursor runs out" re-read the first and only window forever, with no error. Delete the key. limit is the real window and STAYS: it is read end to end (boundary to service to store) and bounded to 1..100, so ask for a wider window instead of a next page. Read the response hasMore to learn whether the window was short — it is now COMPUTED from the engine rather than the constant false it used to be.
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.
refusal
true
optional
Producer-declared: the 5xx this envelope carries is a deliberate refusal whose message is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose message is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — true is the only value.
category
string
optional
Error category (e.g. validation, authorization)
httpStatus
integer
optional
HTTP status of the response carrying this error
details
any
optional
Additional error context (e.g. field validation errors)
Whether more runs matched than this response carries — widen limit to see them. Under status, false means no further match within the scanned window rather than none at all: the window is taken before the filter is applied.
The run the resume was addressed to - the run that failed, and on the stranded arm the run an operator verb can re-arm. Named so a caller acts on an identifier instead of parsing one out of the message
status
Enum<'failed' | 'stranded'>
optional
The engine's own lifecycle verdict for the run, forwarded verbatim when the producer stamped one and never synthesised by the door - absent when the engine reported no status (a subflow child that failed terminally, an engine that predates the discriminator). stranded is the terminally-failed-but-repairable run of AutomationResult.status; failed says the run ran and was rejected. These two terminal-failure members of that union are the only ones that can reach a 400
repairable
boolean
✅
Whether the engine says this run can still be re-armed by an operator verb. Answered from the engine, two ways, and never from the message text: where the engine stamped a status, that word decides (stranded is the run whose OWN pause a resume consumed before a downstream node threw); where it stamped none, the door asks the engine's read-only inspection (IAutomationService.inspectConsumedSuspension) and relays its verdict. The second half is not a fallback but the honest answer for a real exit: a subflow DELEGATION failure carries no status, because nothing re-arms an ancestor by resuming it, and yet the ancestor's consumed pause is journalled and the restore verb re-arms that chain as one unit. Always present on this arm: an absent member would be indistinguishable from a server that predates this field. Every way of not getting an answer is fail-closed - a service that declares no inspection member, and a store the inspection could not read - because promising a repair verb that will refuse is worse than promising nothing
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.
refusal
true
optional
Producer-declared: the 5xx this envelope carries is a deliberate refusal whose message is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose message is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — true is the only value.
category
string
optional
Error category (e.g. validation, authorization)
httpStatus
integer
optional
HTTP status of the response carrying this error
details
any
optional
Additional error context (e.g. field validation errors)
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.
refusal
true
optional
Producer-declared: the 5xx this envelope carries is a deliberate refusal whose message is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose message is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — true is the only value.
category
string
optional
Error category (e.g. validation, authorization)
httpStatus
integer
optional
HTTP status of the response carrying this error
details
any
optional
Additional error context (e.g. field validation errors)
Machine-readable failure classification, set alongside error when the caller must distinguish WHY it failed. A closed union - the members and their transport mappings are documented on the contract (AutomationResult.code, contracts/automation-service.ts).
Lifecycle status. paused means the run suspended at a node and can be continued with the resume route. Absent or completed/failed/stranded/refused means the run reached a terminal state. refused is a first-class refusal: the flow reached an end node declaring outcome: 'refused' — a successful evaluation that said no, so success is true, successMessage is absent and the per-record reason is on refusalMessage; a runner shows it with Close only. stranded is the terminally-failed-but-repairable run: a resume consumed the suspension and a downstream node threw, so the run is recorded as failed and can be re-armed only by an explicit operator verb - never by the resume route, which answers RUN_NOT_FOUND for it.
runId
string
optional
Run id - set when status is paused, so callers can resume it
The screen to render - set when the run paused at a screen node awaiting user input. The client collects values for screen.fields and resumes the run with them.
successMessage
string
optional
Friendly terminal message copied from the flow definition on terminal success, so a screen-flow runner can show a meaningful toast
errorMessage
string
optional
Friendly terminal message copied from the flow definition on failure
flowLabel
string
optional
The flow definition's authored label, copied verbatim so a runner can name the flow (header, completion toast) and translate it against flows.<flow>.label. Set on every result of an evaluation of a registered flow (paused and terminal alike); absent on a refusal carrying code. For a subflow chain it is the addressed (parent) run's flow. Never defaulted to the API name
refusalMessage
string
optional
Rendered refusal, set when status is refused - the end node's message template interpolated against the run's variables, so it names the record. Authored per-record text (not a flow-level copy like the two above); absent on every other status. A runner shows it with Close only
Message carried on AutomationResult for every terminal run (not only screen flows); the screen-flow UI shows it as a toast instead of a generic "Done".
errorMessage
string
optional
Message carried on AutomationResult for every terminal run (not only screen flows); the screen-flow UI shows it as a toast instead of the raw error.
version
integer
optional (default: 1)
Version number
status
Enum<'draft' | 'active' | 'obsolete' | 'invalid'>
optional (default: "draft")
Deployment status
template
never
optional
[REMOVED] flow.template was removed in @objectstack/spec 17.0.0 (audit close-out) — no designer or engine path ever read it, so flagging a flow as a template/subflow did nothing. Delete the key. Shared logic is invoked via a subflow NODE referencing the flow by name. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
[REMOVED] flow.active was removed in @objectstack/spec 17.0.0 (audit close-out) — it never had an effect: the engine arms flows from status, and active: false did NOT stop a flow (worse, the default read as disabled while the engine treated unset as enabled). Delete the key. Use status: 'obsolete' (or 'invalid') to unbind and disable a flow, status: 'active' to arm it. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
runAs
Enum<'system' | 'user'>
optional (default: "user")
Execution identity for the run: system = elevated (bypasses RLS), user = the triggering user (RLS-respecting). A run with no trigger user has no identity to scope to, so under user its data operations are REFUSED — declare system to make the elevation explicit. This covers schedule/time-relative/api triggers AND any record-change flow fired by a write that carried no user.
Flow-level error handling configuration. A durable pause ends the retry-governed segment: strategy: 'retry' describes one synchronous dispatch, so a run that parks on an approval/screen/wait node and later resumes gets one attempt for anything that fails after the pause. Protect the post-pause half with its own failure handling in the flow — a try_catch node's retry around the post-resume work, or fault edges to a handler node.
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.
refusal
true
optional
Producer-declared: the 5xx this envelope carries is a deliberate refusal whose message is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose message is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — true is the only value.
category
string
optional
Error category (e.g. validation, authorization)
httpStatus
integer
optional
HTTP status of the response carrying this error
details
any
optional
Additional error context (e.g. field validation errors)
Message carried on AutomationResult for every terminal run (not only screen flows); the screen-flow UI shows it as a toast instead of a generic "Done".
errorMessage
string
optional
Message carried on AutomationResult for every terminal run (not only screen flows); the screen-flow UI shows it as a toast instead of the raw error.
version
integer
optional (default: 1)
Version number
status
Enum<'draft' | 'active' | 'obsolete' | 'invalid'>
optional (default: "draft")
Deployment status
template
never
optional
[REMOVED] flow.template was removed in @objectstack/spec 17.0.0 (audit close-out) — no designer or engine path ever read it, so flagging a flow as a template/subflow did nothing. Delete the key. Shared logic is invoked via a subflow NODE referencing the flow by name. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
[REMOVED] flow.active was removed in @objectstack/spec 17.0.0 (audit close-out) — it never had an effect: the engine arms flows from status, and active: false did NOT stop a flow (worse, the default read as disabled while the engine treated unset as enabled). Delete the key. Use status: 'obsolete' (or 'invalid') to unbind and disable a flow, status: 'active' to arm it. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
runAs
Enum<'system' | 'user'>
optional (default: "user")
Execution identity for the run: system = elevated (bypasses RLS), user = the triggering user (RLS-respecting). A run with no trigger user has no identity to scope to, so under user its data operations are REFUSED — declare system to make the elevation explicit. This covers schedule/time-relative/api triggers AND any record-change flow fired by a write that carried no user.
Flow-level error handling configuration. A durable pause ends the retry-governed segment: strategy: 'retry' describes one synchronous dispatch, so a run that parks on an approval/screen/wait node and later resumes gets one attempt for anything that fails after the pause. Protect the post-pause half with its own failure handling in the flow — a try_catch node's retry around the post-resume work, or fault edges to a handler node.