State Machine
metadata.stateMachine substrate.
The state machine substrate embeds a named-state transition graph directly in a workflow definition's metadata.stateMachine field. The generic issue.update_status node reads this substrate to validate transitions at runtime, and the lifecycle config uses it as the vocabulary for stage-to-tracker-state mapping.
When to use a state machine
State machines are optional. Use them when:
- Your workflow models an SDLC or business process with distinct, enumerable stages (e.g.
Icebox → Triage → In Progress → Done) - You want the platform to reject illegal transitions rather than silently accepting any string
- You're publishing a group bundle that carries a well-defined lifecycle model to consumer workspaces
Schema
metadata:
stateMachine:
states:
- Icebox
- Triage
- In Progress
- In Review
- Done
- Rejected
initial: Icebox
transitions:
Icebox:
- Triage
Triage:
- In Progress
- Rejected
In Progress:
- In Review
- Rejected
In Review:
- Done
- In Progress
- Rejected
terminal:
- Done
- RejectedProp
Type
Validation rules
validateStateMachine enforces these cross-field invariants (Zod alone cannot express them):
statesis non-empty.initialis a member ofstates.- Every key in
transitionsis a member ofstates. - Every value in
transitions[key]is a member ofstates. - When
terminalis present, every element is a member ofstates.
The validator returns a discriminated union - callers fold the error into the existing issues array without an exception path:
// Success
{ ok: true }
// Failure
{ ok: false; error: "stateMachine.initial \"Started\" is not a member of stateMachine.states" }Runtime helpers
Three helpers are exported for node executors and editor tooling:
import { getNextStates, isValidTransition, isTerminal } from '@/lib/workflow/state-machine'
// States reachable in a single step
getNextStates(sm, 'Triage')
// → ['In Progress', 'Rejected']
// Is a specific transition allowed?
isValidTransition(sm, 'In Progress', 'Done')
// → false (not a direct transition; must go through In Review)
// Is the workflow in a terminal state?
isTerminal(sm, 'Done')
// → truegetNextStates returns [] when the current state has no outgoing transitions; callers check isTerminal separately if that distinction matters.
State machine in group bundles
A state machine embedded in metadata.stateMachine travels with the bundle payload when a group bundle is published. Consumer workspaces that fork the bundle receive the state machine definition as part of the group metadata - they can inspect, extend, or override it after forking.
Tracker-native state names (e.g. Linear's "Started", "In Review") are free-form strings in the state machine. The platform does NOT validate whether those names exist in your actual Linear team workflow - matching is exact and case-sensitive. Use the team's canonical state names to avoid silent mismatches (see Lifecycle Config).
Relationship to lifecycle config
The state machine substrate and spec.lifecycle are complementary:
- State machine - defines WHAT states exist and which transitions are legal. Lives in
metadata.stateMachine. Validated at publish time. - Lifecycle config - defines WHEN each stage fires (tracker-native transition trigger) and WHAT happens when it exits. Lives in
spec.lifecycle. References state names that should match the state machine vocabulary.
Using both together gives you a fully modelled SDLC lifecycle with validated transitions.
Example: software delivery lifecycle
metadata:
stateMachine:
states: [Backlog, In Progress, In Review, QA, Accepted, Rejected]
initial: Backlog
transitions:
Backlog: [In Progress]
In Progress: [In Review, Rejected]
In Review: [QA, In Progress]
QA: [Accepted, In Progress, Rejected]
terminal: [Accepted, Rejected]
spec:
lifecycle:
development:
trigger:
event: issue.transitioned
when: { to: "In Progress" }
exit:
transition_to: "In Review"
on_fail: "Rejected"
qa:
trigger:
event: issue.transitioned
when: { to: "QA" }
exit:
transition_to: "Accepted"
on_fail: "Rejected"Related pages
- Lifecycle Config - stage-to-tracker-state mapping
- Group Bundles - state machines travel with bundles
- SDLC Default Template - production SDLC using this substrate