A JSON specification for statecharts and workflows. Expressive enough for real-world state machines, with built-in expression evaluation and converters to XState.
npm install @statelyai/schema{
"id": "order",
"version": "1.0.0",
"queryLanguage": "jmespath",
"initial": "pending",
"context": { "retries": 0 },
"states": {
"pending": {
"on": {
"SUBMIT": {
"target": "processing",
"guard": "{{ context.items }}"
}
}
},
"processing": {
"invoke": [{
"src": "processOrder",
"retry": { "maxAttempts": 3, "interval": "PT2S", "backoff": 2 },
"onDone": { "target": "complete" },
"onError": [
{ "target": "pending", "guard": "{{ context.retries < 3 }}" },
{ "target": "failed" }
]
}]
},
"complete": { "type": "final" },
"failed": { "type": "final" }
}
}Values wrapped in {{ }} are evaluated at runtime using the configured queryLanguage:
| Language | Package | Sync | Example |
|---|---|---|---|
jmespath |
jmespath | Yes | {{ context.count }} |
jsonpath |
jsonpath-plus | Yes | {{ $.context.count }} |
jsonata |
jsonata | No (async) | {{ context.count + 1 }} |
Expressions receive { context, event } as their data root.
Each query language has its own entry point with a converter:
import { jmespathToXStateMachine } from '@statelyai/schema/jmespath';
import { transition, initialTransition } from 'xstate';
const machine = jmespathToXStateMachine(spec);
const [state] = initialTransition(machine);
const [next] = transition(machine, state, { type: 'SUBMIT' });Available converters:
// Pick one:
import { jmespathToXStateMachine, jmespathToXStateConfig } from '@statelyai/schema/jmespath';
import { jsonpathToXStateMachine, jsonpathToXStateConfig } from '@statelyai/schema/jsonpath';
import { jsonataToXStateMachine, jsonataToXStateConfig } from '@statelyai/schema/jsonata';
// Or bring your own evaluator:
import { toXStateMachine, toXStateConfig } from '@statelyai/schema';
import type { ExpressionEvaluator } from '@statelyai/schema';
const evaluate: ExpressionEvaluator = (expression, data) => { /* ... */ };
const machine = toXStateMachine(spec, evaluate);Zod schemas for runtime validation (requires zod peer dependency):
import { machineSchema } from '@statelyai/schema';
const result = machineSchema.safeParse(json);JSON Schema files are also available for editor tooling:
import machineJsonSchema from '@statelyai/schema/machine.json';See the full Stately Machine Specification for formal definitions, conformance requirements, and detailed semantics.
| Property | Type | Description |
|---|---|---|
id |
string |
State node ID |
type |
"parallel" | "history" | "final" |
State type (omit for normal states) |
initial |
string |
Initial child state |
states |
Record<string, State> |
Child states |
on |
Record<string, Transition> |
Event-driven transitions |
after |
Record<string, Transition> |
Delayed transitions (ms or ISO 8601 duration) |
always |
Transition |
Eventless transitions |
entry |
Action[] |
Actions run on state entry |
exit |
Action[] |
Actions run on state exit |
invoke |
Invoke[] |
Actors spawned on entry |
tags |
string[] |
State tags |
output |
expression | any |
Output for final states |
history |
"shallow" | "deep" |
History type (when type: "history") |
target |
string |
Default target for history states |
description |
string |
Human-readable description |
meta |
Record<string, any> |
Arbitrary metadata |
Extends State with:
| Property | Type | Description |
|---|---|---|
version |
string |
Machine version |
queryLanguage |
"jsonata" | "jmespath" | "jsonpath" |
Expression language |
context |
Record<string, any> |
Initial context values |
input |
JSONSchema |
JSON Schema for machine input |
schemas |
{ context?, events? } |
JSON Schema definitions for context and events |
A transition is an object or an array of objects (for branching):
{
"on": {
"NEXT": { "target": "step2" },
"SUBMIT": {
"target": "processing",
"guard": "{{ context.isValid }}",
"context": { "submitted": true },
"actions": [{ "type": "xstate.log" }]
},
"CHECK": [
{ "target": "high", "guard": "{{ context.value > 100 }}" },
{ "target": "low" }
]
}
}| Property | Type | Description |
|---|---|---|
target |
string |
Target state |
guard |
expression | NamedGuard |
Condition for taking transition |
context |
Record<string, expression | any> |
Context assignments (appended as xstate.assign) |
actions |
Action[] |
Actions to execute |
description |
string |
Human-readable description |
meta |
Record<string, any> |
Arbitrary metadata |
order |
number |
Explicit transition priority |
Built-in actions are prefixed with xstate.:
{ "type": "xstate.assign", "params": { "count": "{{ context.count + 1 }}" } }
{ "type": "xstate.raise", "params": { "event": { "type": "DONE" } } }
{ "type": "xstate.sendTo", "params": { "actorRef": "worker", "event": { "type": "PING" } } }
{ "type": "xstate.log", "params": { "message": "{{ context.status }}" } }
{ "type": "xstate.emit", "params": { "event": { "type": "NOTIFY" } } }Custom actions pass through to xstate (resolved via setup()):
{ "type": "trackAnalytics", "params": { "event": "checkout" } }Expression guard:
{ "guard": "{{ context.count > 0 }}" }Named guard (resolved via setup()):
{ "guard": { "type": "isValid", "params": { "min": 5 } } }| Property | Type | Description |
|---|---|---|
src |
string |
Actor source (resolved via setup()) |
id |
string |
Actor ID |
input |
expression | any |
Input passed to actor |
onDone |
Transition |
Transition when actor completes |
onError |
Transition |
Transition when actor fails |
onSnapshot |
Transition |
Transition on actor snapshot |
timeout |
string |
ISO 8601 duration |
heartbeat |
string |
ISO 8601 duration |
retry |
{ maxAttempts, interval?, backoff? } |
Retry policy on error |
Keys can be millisecond strings or ISO 8601 durations. The converter parses ISO 8601 to ms automatically:
{
"after": {
"1000": { "target": "next" },
"PT30S": { "target": "timeout" },
"PT1H": { "target": "expired" }
}
}MIT