Skip to content

Tutorial: Visualize a state machine in Stately

Turn an @typeonce/effect-machine machine into an interactive statechart in stately.ai/viz — without adopting XState as your runtime. This walkthrough uses the send-money lifecycle sample that ships with the docs.

For the full reading contract (state trees, targets, MachineJSON, coverage gate), see State Machines.

  1. Start from the sample — a machine that mirrors the transfer workflow
  2. Export an XState createMachine config with effect-analyze
  3. Paste it into the Stately visualizer
  4. Read the resulting diagram — Advance path, Fail sinks, invoke, finals

The machine lives next to the Effect pipeline it models:

apps/docs/samples/observability-transfer/transfer-lifecycle.ts

Dense Advance / Fail events at every active stage, branch conditions the analyzer reads as guards, an invoked child, and two explicit finals:

export const TransferStates = Machine.defineStates({
Validating,
FetchingRate,
Converting,
Executing,
Confirming,
Done: { schema: Done, type: 'final' },
Failed: { schema: Failed, type: 'final' },
})
export const TransferLifecycle = Machine.make({
states: TransferStates.states,
events: [Advance, Fail, Executed],
initial: () => TransferStates.initial.Validating(new Validating()),
}).handle({
// …
Converting: {
on: {
Advance: ({ state, target }) =>
state.sufficientFunds
? target.full.Executing(new Executing())
: target.full.Failed(new Failed()),
Fail: ({ target }) => target.full.Failed(new Failed()),
},
},
Executing: {
invoke: () => ExecuteTransfer,
on: {
Executed: ({ target }) =>
target.full.Confirming(new Confirming({ retryable: true })),
Fail: ({ target }) => target.full.Failed(new Failed()),
},
},
// …
})

The Effect program in send-money-workflow.ts still does the real work. This machine is the model the analyzer diagrams and checks.

From the repo root (after installing / building the CLI):

Terminal window
npx effect-analyze \
./apps/docs/samples/observability-transfer/transfer-lifecycle.ts \
--format xstate-config

You get a paste-ready module. The same config is also embedded in the local visualizer page:

Terminal window
npx effect-analyze \
./apps/docs/samples/observability-transfer/transfer-lifecycle.ts \
--format statechart-html --open
  1. Open stately.ai/viz
  2. Replace the editor contents with the generated config (below)
  3. Click Visualize
import { createMachine } from 'xstate';
export const TransferLifecycleMachine = createMachine({
id: 'TransferLifecycle',
initial: 'Validating',
states: {
Validating: { on: { Advance: 'FetchingRate', Fail: 'Failed' } },
FetchingRate: { on: { Advance: 'Converting', Fail: 'Failed' } },
Converting: { on: { Advance: [{ target: 'Executing', guard: 'state.sufficientFunds' }, { target: 'Failed', guard: '!(state.sufficientFunds)' }], Fail: 'Failed' } },
Executing: { invoke: { src: 'executeTransfer', id: 'executeTransfer' }, on: { Executed: 'Confirming', Fail: 'Failed' } },
Confirming: { on: { Advance: 'Done', Fail: [{ target: 'Confirming', guard: 'state.retryable' }, { target: 'Done', guard: '!(state.retryable)' }] } },
Done: { type: 'final' },
Failed: { type: 'final' }
}
});

TransferLifecycle machine in the Stately visualizer

In the diagram:

  • Happy pathValidatingFetchingRateConvertingExecutingConfirmingDone
  • Fail sinksFail from active stages into the Failed final
  • InvokeExecuting shows INVOKE / executeTransfer, with the child’s Executed event carrying the machine forward
  • Guarded choicesConverting has two Advance edges and Confirming two Fail edges, one per branch (the legacy visualizer draws the alternatives but does not print the guard text; it is in the config)
  • FinalsDone and Failed render as final states

The machine is live in the visualizer: the Events panel only enables the events the current state handles, so you can step through the workflow and confirm the targets resolve.

After you change the machine, gate structural completeness on the sample:

Terminal window
npx effect-analyze \
./apps/docs/samples/observability-transfer/transfer-lifecycle.ts \
--format statechart-coverage --min-coverage 60

Next: the full reading contract, hierarchy, and coverage semantics in State Machines.