ObjectStackObjectStack

Flow

Flow protocol schemas

Source: packages/spec/src/automation/flow.zod.ts

TypeScript Usage

import { FlowSchema, FlowEdgeSchema, FlowNodeSchema, FlowNodeAction, FlowVariableSchema, FlowVersionHistorySchema } from '@objectstack/spec/automation';
import type { Flow, FlowEdge, FlowNode, FlowNodeAction, FlowVersionHistory } from '@objectstack/spec/automation';

// Validate data
const result = FlowSchema.parse(data);

Flow

Properties

PropertyTypeRequiredDescription
namestringMachine name
labelstringFlow label
descriptionstringoptional
successMessagestringoptionalMessage 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".
errorMessagestringoptionalMessage 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.
versionintegeroptional (default: 1)Version number
statusEnum<'draft' | 'active' | 'obsolete' | 'invalid'>optional (default: "draft")Deployment status
templateneveroptional[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; apply them by hand.
typeEnum<'autolaunched' | 'record_change' | 'schedule' | 'screen' | 'api'>Flow type
variables{ name: string; type: string; isInput?: boolean; isOutput?: boolean; … }[]optionalFlow variables
nodes{ id: string; type: string; label: string; config?: Record<string, any>; … }[]Flow nodes
edges{ id: string; source: string; target: string; condition?: string | object; … }[]Flow connections
activeneveroptional[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; apply them by hand.
runAsEnum<'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.
errorHandling{ strategy?: Enum<'fail' | 'retry' | 'continue'>; maxRetries?: integer; backoffMs?: integer; backoffMultiplier?: number; … }optionalFlow-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.
protection{ lock: Enum<'none' | 'no-overlay' | 'no-delete' | 'full'>; reason: string; docsUrl?: string }optionalPackage author protection block — lock policy for this flow.
_lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>optionalItem-level lock — controls overlay & delete (ADR-0010).
_lockReasonstringoptionalHuman-readable reason shown when a write is refused by _lock.
_lockSourceEnum<'artifact' | 'package' | 'env-forced'>optionalLayer that set _lock (artifact | package | env-forced).
_provenanceEnum<'package' | 'org' | 'env-forced'>optionalOrigin of the item (package | org | env-forced).
_packageIdstringoptionalOwning package machine id.
_packageVersionstringoptionalOwning package version.
_lockDocsUrlstringoptionalOptional documentation link surfaced next to _lockReason.

Nested Shape: Flow.variables[number]

PropertyTypeRequiredDescription
namestringVariable name
typestringData type (text, number, boolean, object, list)
isInputbooleanoptional (default: false)Is input parameter
isOutputbooleanoptional (default: false)Is output parameter
defaultValueanyoptionalValue 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.

Nested Shape: Flow.nodes[number]

PropertyTypeRequiredDescription
idstringNode unique ID
typestringAction 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.
labelstringNode label
configRecord<string, any>optionalNode configuration
connectorConfig{ connectorId: string; actionId: string; input?: Record<string, any> }optional
position{ x: number; y: number }optional
timeoutMsintegeroptionalMaximum execution time for this node in milliseconds
inputSchemaRecord<string, { type: Enum<'string' | 'number' | 'boolean' | 'object' | 'array'>; required?: boolean; description?: string }>optionalInput parameter schema for this node
outputSchemaneveroptional[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; apply them by hand.
waitEventConfig{ eventType: Enum<'timer' | 'signal' | 'webhook' | 'manual' | 'condition'>; timerDuration?: string; signalName?: string }optionalConfiguration for wait node event resumption
boundaryConfig{ attachedToNodeId: string; eventType: Enum<'error' | 'timer' | 'signal' | 'cancel'>; interrupting?: boolean; errorCode?: string; … }optionalConfiguration for boundary events attached to host nodes

Nested Shape: Flow.edges[number]

PropertyTypeRequiredDescription
idstringEdge unique ID
sourcestringSource Node ID
targetstringTarget Node ID
conditionstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object }optionalPredicate (CEL) returning boolean used for branching.
typeEnum<'default' | 'fault' | 'conditional' | 'back'>optional (default: "default")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)
labelstringoptionalLabel on the connector
isDefaultbooleanoptional (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.

Nested Shape: Flow.errorHandling

PropertyTypeRequiredDescription
strategyEnum<'fail' | 'retry' | 'continue'>optional (default: "fail")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.
maxRetriesintegeroptional (default: 0)Retry attempts after the initial one. Read only under strategy: 'retry', which requires >= 1; 0 (the default) means no retry.
backoffMsintegeroptional (default: 1000)Base delay before the first retry (ms); subsequent delays multiply by backoffMultiplier
backoffMultipliernumberoptional (default: 1)Exponential backoff multiplier; 1 (the default) keeps the delay flat
maxRetryDelayMsintegeroptional (default: 30000)Ceiling for a single backoff delay (ms)
jitterbooleanoptional (default: false)Randomize each delay within [50%, 100%] of its computed value — spreads a thundering herd of simultaneous retries
retryDelayMsneveroptional[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; apply them by hand.
fallbackNodeIdneveroptional[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; apply them by hand.

Nested Shape: Flow.protection

PropertyTypeRequiredDescription
lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>Lock policy — none | no-overlay | no-delete | full.
reasonstringUser-visible reason shown when the lock blocks an action.
docsUrlstringoptionalOptional URL the Studio banner links to for more context.

FlowEdge

Properties

PropertyTypeRequiredDescription
idstringEdge unique ID
sourcestringSource Node ID
targetstringTarget Node ID
conditionstring | { dialect: Enum<'cel' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object }optionalPredicate (CEL) returning boolean used for branching.
typeEnum<'default' | 'fault' | 'conditional' | 'back'>optional (default: "default")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)
labelstringoptionalLabel on the connector
isDefaultbooleanoptional (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.

FlowNode

Properties

PropertyTypeRequiredDescription
idstringNode unique ID
typestringAction 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.
labelstringNode label
configRecord<string, any>optionalNode configuration
connectorConfig{ connectorId: string; actionId: string; input?: Record<string, any> }optional
position{ x: number; y: number }optional
timeoutMsintegeroptionalMaximum execution time for this node in milliseconds
inputSchemaRecord<string, { type: Enum<'string' | 'number' | 'boolean' | 'object' | 'array'>; required?: boolean; description?: string }>optionalInput parameter schema for this node
outputSchemaneveroptional[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; apply them by hand.
waitEventConfig{ eventType: Enum<'timer' | 'signal' | 'webhook' | 'manual' | 'condition'>; timerDuration?: string; signalName?: string }optionalConfiguration for wait node event resumption
boundaryConfig{ attachedToNodeId: string; eventType: Enum<'error' | 'timer' | 'signal' | 'cancel'>; interrupting?: boolean; errorCode?: string; … }optionalConfiguration for boundary events attached to host nodes

Nested Shape: FlowNode.connectorConfig

PropertyTypeRequiredDescription
connectorIdstringRegistered connector name
actionIdstringAction key declared by the connector
inputRecord<string, any>optionalMapped inputs for the action

Nested Shape: FlowNode.inputSchema[string]

PropertyTypeRequiredDescription
typeEnum<'string' | 'number' | 'boolean' | 'object' | 'array'>Parameter type
requiredbooleanoptional (default: false)Whether the parameter is required
descriptionstringoptionalParameter description

Nested Shape: FlowNode.waitEventConfig

PropertyTypeRequiredDescription
eventTypeEnum<'timer' | 'signal' | 'webhook' | 'manual' | 'condition'>What kind of event resumes the execution
timerDurationstringoptionalISO 8601 duration (e.g., "PT1H") or wait time for timer events
signalNamestringoptionalNamed signal or webhook event to wait for
timeoutMsneveroptional[REMOVED] waitEventConfig.timeoutMs was removed in @objectstack/spec 17. It documented a timeout guard that never existed: nothing ever failed or resumed a wait on a deadline. Its only reader treated it as the timer DURATION when timerDuration was absent, so use timerDuration — but QUOTE the number: the key is a string, and a bare numeric string is read as milliseconds, making timeoutMs: 60000 and timerDuration: '60000' the same wait (timerDuration: 'PT1M' is the ISO 8601 spelling of that same 60s). Stored flows are converted automatically — the conversion does the quoting for you. Run os migrate meta --from 16 to list the mechanical edits for existing sources; apply them by hand.
onTimeoutneveroptional[REMOVED] waitEventConfig.onTimeout was removed in @objectstack/spec 17. It had no readers at all — no code path ever inspected it, so neither fail nor continue ever happened. Delete the key. There is no replacement: wait has no timeout, and a wait node resumes only when its timer elapses or its signal arrives. Run os migrate meta --from 16 to list the mechanical edits for existing sources; apply them by hand.

Nested Shape: FlowNode.boundaryConfig

PropertyTypeRequiredDescription
attachedToNodeIdstringHost node ID this boundary event monitors
eventTypeEnum<'error' | 'timer' | 'signal' | 'cancel'>Boundary event trigger type
interruptingbooleanoptional (default: true)If true, the host activity is cancelled when this event fires
errorCodestringoptionalSpecific error code to catch (empty = catch all errors)
timerDurationstringoptionalISO 8601 duration for timer boundary events
signalNamestringoptionalNamed signal to catch

FlowNodeAction

Allowed Values

  • start
  • end
  • decision
  • assignment
  • loop
  • create_record
  • update_record
  • delete_record
  • get_record
  • http
  • notify
  • script
  • screen
  • wait
  • subflow
  • map
  • connector_action
  • parallel_gateway
  • join_gateway
  • boundary_event

FlowVariable

Properties

PropertyTypeRequiredDescription
namestringVariable name
typestringData type (text, number, boolean, object, list)
isInputbooleanoptional (default: false)Is input parameter
isOutputbooleanoptional (default: false)Is output parameter
defaultValueanyoptionalValue 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.

FlowVersionHistory

Properties

PropertyTypeRequiredDescription
flowNamestringFlow machine name
versionintegerVersion number
definition{ name: string; label: string; description?: string; successMessage?: string; … }Complete flow definition snapshot
createdAtstringWhen this version was created
createdBystringoptionalUser who created this version
changeNotestringoptionalDescription of what changed in this version

Nested Shape: FlowVersionHistory.definition

PropertyTypeRequiredDescription
namestringMachine name
labelstringFlow label
descriptionstringoptional
successMessagestringoptionalMessage 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".
errorMessagestringoptionalMessage 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.
versionintegeroptional (default: 1)Version number
statusEnum<'draft' | 'active' | 'obsolete' | 'invalid'>optional (default: "draft")Deployment status
templateneveroptional[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; apply them by hand.
typeEnum<'autolaunched' | 'record_change' | 'schedule' | 'screen' | 'api'>Flow type
variables{ name: string; type: string; isInput?: boolean; isOutput?: boolean; … }[]optionalFlow variables
nodes{ id: string; type: string; label: string; config?: Record<string, any>; … }[]Flow nodes
edges{ id: string; source: string; target: string; condition?: string | object; … }[]Flow connections
activeneveroptional[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; apply them by hand.
runAsEnum<'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.
errorHandling{ strategy?: Enum<'fail' | 'retry' | 'continue'>; maxRetries?: integer; backoffMs?: integer; backoffMultiplier?: number; … }optionalFlow-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.
protection{ lock: Enum<'none' | 'no-overlay' | 'no-delete' | 'full'>; reason: string; docsUrl?: string }optionalPackage author protection block — lock policy for this flow.
_lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>optionalItem-level lock — controls overlay & delete (ADR-0010).
_lockReasonstringoptionalHuman-readable reason shown when a write is refused by _lock.
_lockSourceEnum<'artifact' | 'package' | 'env-forced'>optionalLayer that set _lock (artifact | package | env-forced).
_provenanceEnum<'package' | 'org' | 'env-forced'>optionalOrigin of the item (package | org | env-forced).
_packageIdstringoptionalOwning package machine id.
_packageVersionstringoptionalOwning package version.
_lockDocsUrlstringoptionalOptional documentation link surfaced next to _lockReason.

On this page