Stateful Workflow Profile
The Stateful Workflow Profile defines event-driven workflows as a sequence of named state entries. It extends Workfile Core with state scopes, typed data transfer, subscriptions, timers, and transitions.
The Stateful Workflow Profile does not change action, expression, step, outcome, or single-assignment semantics. Use of the top-level states key structurally activates this profile; no separate declaration is permitted.
A Core Validator MUST NOT reinterpret states as a step list. Validation ownership and target support are defined under Profile Conformance. Persistent suspension, recovery, compensation, and continuation after loss of process state are governed by the Durable Execution Profile rather than implied by this profile alone.
State Document Form
Section titled “State Document Form”A state-form Workfile declares states, a nonempty map from state names to state declarations, and initial, the name of one declared state. It MUST NOT declare top-level steps.
State names follow the ordinary name syntax and are labels, not expression bindings.
inputs: approval_channel: { type: string, required: true }
initial: awaiting_approval
states: awaiting_approval: on_enter: - notify: { messaging.post: { channel: approvals, text: Ready for review. } } on: decision: event: messaging.approval configuration: { channel: "{{ inputs.approval_channel }}" } match: { request_id: "{{ notify.id }}" } route: "{{ decision.action }}" cases: approve: { goto: provisioning, into: { approver: "{{ decision.user }}" } } reject: { goto: closed } else: { goto: escalated } timeout: { after: 24h, goto: escalated }
provisioning: accepts: approver: { type: string, required: true } steps: - account: { crm.provision: { owner: "{{ approver }}" } } goto: closed
escalated: steps: [{ page: { messaging.post: { channel: managers, text: Approval timed out. } } }] goto: closed
closed: { final: true }A state declaration admits only accepts, on_enter, steps, on, timeout, final, goto, into, route, cases, else, recovery, and extension keys, subject to the final-state and immediate-transition exclusions below. recovery is governed by the Durable Execution Profile and activates it.
on_enter and steps are optional step lists and default to empty. They are respectively the pre-correlation and post-subscription phases; their names do not describe subscription activation.
On each state entry, subscription configuration and activation precede on_enter; on_enter then runs, correlation and the timer are fixed, and steps runs while eligible events are retained. If either list fails or terminates the run, no transition is selected except as provided by applicable failure policy.
After both lists succeed, an immediate goto or route is evaluated; otherwise the entry waits for an eligible subscription or timeout. final: true declares a terminal state, and final MUST NOT be false; omission declares a nonfinal state.
A final state can declare accepts, on_enter, steps, and recovery, but MUST NOT declare on, timeout, goto, into, route, cases, or else. After its step lists succeed, the run completes successfully and workflow outputs are evaluated in that final entry’s scope.
Static validation checks outputs across all declared final-entry scopes using the cross-branch rules of wf.route: a reference is permitted when its binding exists in the root or at least one final-entry scope; its type is the join across scopes where it exists; no available join or absence from every final-entry and root scope is invalid; and absence from one or more final-entry scopes makes the joined type nullable.
This normal-completion shape contributes to the successful workflow result join. Explicit stops use their own lexical scopes under wf.stop.
A nonfinal state MUST declare at least one immediate transition, subscription, or timeout. A state that declares an immediate goto or route MUST NOT declare on or timeout; a violation is invalid with document.control.
initial and every transition target MUST name a declared state, and at least one final state MUST be reachable from initial. Every directed cycle in the state graph MUST contain an event or timeout transition; immediate transitions alone MUST NOT create an unbounded execution loop.
State Inputs and Entry Steps
Section titled “State Inputs and Entry Steps”accepts is a closed map from names to field declarations and describes values received from a transition’s into projection. The initial state MUST NOT declare accepts.
For every transition to another state, static validation MUST reject an undeclared supplied name, an omitted required name, or a statically incompatible value. An omitted name uses its declared default; an optional name without a default follows the state-accepts row of Field Declarations.
After evaluating into and applying defaults, the executor MUST validate every supplied or defaulted value against the target’s complete accepts declaration before binding any target value or beginning the target entry. A mismatch causes fault.type in the source entry and prevents the transition without creating a partial target entry.
Every entry, including reentry into the same state, creates a new nested scope rather than reassigning an earlier scope. Its accepted values bind directly by name, followed by the bindings created by on_enter and steps.
The entry can read root inputs, run, root trigger bindings, and other root bindings under ordinary scope rules. A state entry MUST NOT read a binding from a prior entry or another state unless a transition carries the value through into and the target declares it in accepts.
A state name is not a readable binding. Entry positions are counted from zero separately for each state and form step paths such as awaiting_approval[2].notify.
Implementations MUST preserve the identity and outcome of every entry even when the same state is visited repeatedly. into is a projection evaluated atomically in the source entry’s scope after its transition has been selected.
For an event transition, the event payload binding is also in scope. A projection fault prevents the transition and fails the state entry under ordinary failure semantics; it MUST NOT create a partially initialized target entry.
A failure in an entry step follows base retry and failure policy and any applicable Durable recovery. wf.stop and wf.fail terminate the whole run, not merely the current entry.
Cancellation of an entry cancels its in-progress work and prevents a later transition from that entry.
Transitions and Correlation
Section titled “Transitions and Correlation”A transition is either direct or routed. A direct transition contains required goto and optional into.
A routed transition contains route and cases, plus else exactly when required below. Subject typing, closed-enum validation, and case selection follow wf.route.
cases is a nonempty map from distinct literal string labels to nested transitions, and else, when present, is a nested transition. A transition map MUST contain exactly one of goto and route; cases and else appear only with route, and into appears only with goto.
For a subject without a statically established closed enum domain, else is REQUIRED. A route fault fails the state entry. Affected state control evaluations follow dry-run termination.
A transition can appear as the immediate exit of a state, as a state’s timeout, or within a subscription. timeout contains after plus one transition.
after evaluates after on_enter succeeds and MUST produce a nonnegative duration or a timestamp. A duration is measured from that evaluation; a timestamp names the deadline directly.
The timeout becomes eligible no earlier than its deadline and remains pending while steps completes. on maps subscription binding names to subscription declarations.
Each declaration contains event, match, and one transition, and can contain configuration and connection. event is a literal connector.trigger reference whose resolved manifest kind is external or poll; schedule triggers are not state subscriptions.
configuration is a trigger configuration governed by Trigger Configuration. The connection sibling of event is governed by Connection Selection.
match is a nonempty map that MUST supply at least one name from the trigger’s correlate_on declaration, MUST NOT name another field, and contains expression-bearing values that MUST satisfy their payload-field types. The subscription name binds the delivered payload directly in the current entry scope for transition selection and into; it does not bind the {status, event} scope produced by wf.wait_for.
Subscription names MUST be distinct from accepted values and step ids in the entry. When the state is entered, each subscription’s configuration is evaluated over root inputs, validated, and fixed for that entry.
A configuration fault fails the entry before on_enter begins. Once configuration succeeds, the subscription becomes active.
The subscription’s match expressions are evaluated atomically after on_enter succeeds, then fixed for that entry. An implementation MUST retain a candidate event accepted after activation while correlation evaluation or steps is in progress.
An event accepted before activation is not eligible for that subscription. It MUST NOT interrupt a running step list; an eligible event is delivered only after both lists succeed.
An event is eligible when every supplied match value equals the corresponding payload field. Fields declared by correlate_on but omitted from match do not constrain delivery.
One event can be eligible for subscriptions in more than one run; the implementation MUST apply it independently to each match and MUST NOT choose an implicit winner. Within one entry, an event MUST NOT be delivered more than once.
An event for which the current entry has no matching subscription does not change the run. A delivery that matches more than one subscription in the same entry is ambiguous configuration and MUST be rejected at activation when overlap can be established statically; if it arises only from runtime values, the entry fails with state.ambiguous_subscription rather than selecting by map order.
The first eligible event or timeout selects the transition, ordered by recorded event acceptance time and the timeout deadline. An event accepted no later than the deadline wins over that timeout; otherwise the timeout wins.
Among eligible events accepted at the same recorded instant, delivery order is the stable acceptance order of their initiation capability. Once selected, the entry unsubscribes, cancels its timer, evaluates the selected transition, and creates exactly one target entry.
Each subscription structurally requires the initiation capability corresponding to its trigger kind. The unsupported-workflow prohibition under Static Validation applies to a target lacking that capability or Stateful Workflow Profile support.
Correlation state, pending event identity, selected transition, and state resumption after interruption require the Durable Execution Profile when the deployment claims durable continuation; this profile alone defines their logical result, not their persistence representation.