Skip to content

The states Profile

For a long-running process whose shape is event-driven rather than sequential, a Workfile MAY declare states instead of steps.

initial: awaiting_approval
states:
awaiting_approval:
on_enter: [{ notify: { slack.post: { channel: "#sales-ops" } } }]
timeout: { after: 24h, goto: escalated }
on:
decision:
event: slack.approval
match: { message: "{{ notify.ts }}" }
route: "{{ decision.action }}"
cases:
approve: { goto: provisioning, into: { approver: "{{ decision.user }}" } }
reject: { goto: closed }
amend: { goto: awaiting_approval, reset_timeout: true }
else: { goto: escalated }
provisioning:
accepts:
approver: { type: "string", required: true }
steps: [{ account: { crm.provision: { owner: "{{ approver }}" } } }]
goto: closed
escalated:
on_enter: [{ page: { slack.post: { channel: "#sales-leads" } } }]
goto: closed
closed: { final: true }

Each state declares the events that are valid in it, and the transition that each causes. on_enter and steps are ordinary step lists, and follow every rule above. A state MAY declare recovery, which applies to its step lists under the scope recovery rules.

Implementations MUST verify that initial names a declared state. Implementations MUST verify that every goto names a declared state. Implementations MUST verify that at least one final state is reachable from the initial state.

Transitions. A transition names the next state. It takes one of two forms.

  • goto names one state. It accepts into and reset_timeout: true.
  • route takes cases and a required else. Each case maps one value of the routing expression to a transition. The rules of route apply: cases are unordered, dispatch is by equality, and a subject with a declared enum domain MUST be covered exhaustively.

A transition appears in three places: an on entry, a timeout, and a state’s own exit after its steps complete. timeout.after accepts a duration or a timestamp, literal or expression. Dispatch that needs a comparison belongs in the steps of a state, ahead of a route over the result.

Subscriptions. An on entry declares one subscription. Its key names the binding for the event payload, which is in scope for that entry’s route and into expressions. event names one event, connector.event, from the catalog. match supplies the expected value of every correlation key that the event’s manifest declares. Implementations MUST NOT deliver an event to a run whose values do not match. Two entries MAY name one event with different correlation values.

A subscription binds the payload directly, and not the two-member scope of a wait_for. A subscription has no timeout of its own. A state’s own timeout covers that case, and it is a transition rather than a binding.

A state’s subscriptions are active only while the run is in that state. The run subscribes on entry, and unsubscribes on exit. Implementations MUST evaluate correlation values when on_enter completes, because those values can reference its results. Implementations MUST hold a correlated event that arrives after the run entered the state, and MUST deliver it once evaluation completes. Implementations MUST NOT interrupt a running step list: an event that arrives during on_enter or steps is delivered when the list completes. An event that the current state does not name MUST NOT change the run’s state, and implementations SHOULD record its arrival.

Data across a transition. A transition carries data with into: a projection of expressions in the source scope into bindings of the target. The target state declares what it takes in accepts, as field declarations. Every transition into a state MUST supply every name that its accepts declares required, and MUST NOT supply a name that accepts does not declare. Declared names bind in the target’s scope, and are read directly, as {{ approver }}. The initial state MUST NOT declare accepts, because no transition enters it.

State entries and scope. A run can enter one state more than once. Each entry creates a scope, exactly as an iteration of for_each does. Within a state, a step reads the bindings of the current entry by name, and reads the root scope by the scope rules. Bindings of one entry are invisible to every other entry and to every other state. A state name is not a readable path, because single assignment requires a binding to hold one value. Data that must outlive an entry crosses on a transition, in into. The record identifies each entry by position, so a run’s history holds every entry separately.

Outputs. Implementations MUST evaluate a states file’s outputs in the scope of the final state’s entry, which nests inside the root scope. Data that the result needs therefore reaches the final state through into and accepts.

A steps file is a states file with a single state.

states-profile is a capability.