Skip to main content

State Machine

Project-scoped state machine designs with a two-role workflow: developers define the vocabulary, product managers wire it into diagrams. Available in the web UI at embedhub.com/tools/StateMachine.

Useful for firmware control flows, onboarding flows, protocol handshakes — anything where a designer and an implementer need to agree on what states exist and how the system moves between them.

The two-role model

A state machine in EmbedHub is split into two files in your project:

FileOwnerContains
state-machines/palette/palette.yamlDeveloperThe list of legal states and events
state-machines/diagrams/<slug>.yamlProduct managerA diagram wiring palette states with transitions

There is exactly one palette per project, and as many diagrams as you want — for example happy-path.yaml, error-recovery.yaml, setup.yaml — all sharing the same vocabulary.

A diagram can only use states and events that the palette declares. Saving a diagram that references something the palette doesn't define is rejected with a field-level validation error.

The palette

A small YAML file listing the states and events your firmware (or system) supports. The developer owns it because each id corresponds to something they've actually implemented. The palette is authored and updated via the EmbedHub CLI — the web UI only displays it.

version: "2.0"
states:
- id: init
description: "Boot-time initialization"
- id: calibration
description: "Sensor calibration routine"
- id: idle
description: "Ready, waiting for activity"
- id: sleep
description: "Low-power state"

events:
- id: init_success
description: "Initialization completed without errors"
- id: calibration_success
description: "Calibration finished within tolerance"
- id: calibration_fail
description: "Calibration outside tolerance; retry from init"
- id: idle_timeout
description: "No activity for the configured idle window"

Field rules:

  • id is lowercase, starts with a letter or digit, then letters, digits, or _. Must be unique within states (and within events). Ids are also what diagrams reference and what shows up on the canvas — keep them short and readable.
  • description is optional, shown next to the id when picking events in the diagram editor.

A diagram

Each diagram is a separate YAML file. The slug in the filename becomes the diagram's identifier in the UI and in URLs.

version: "2.0"
name: "Boot and idle"
description: "Power-on through calibration into the idle/sleep loop"

initial_state: init

transitions:
- from: init
to: calibration
event: init_success
- from: calibration
to: idle
event: calibration_success
- from: calibration
to: sleep
event: calibration_fail
- from: idle
to: sleep
event: idle_timeout

layout:
nodes:
- id: init
x: 120
y: 80
- id: calibration
x: 320
y: 80
- id: idle
x: 520
y: 80
- id: sleep
x: 720
y: 80

What gets validated when you save:

  • Every from, to, and event must exist in the palette.
  • initial_state must be one of the palette's states.
  • name is required.

layout is editor-managed positions for the drag-and-drop canvas and doesn't affect the semantics — feel free to omit it when writing by hand.

A paired <slug>.svg is generated and saved alongside the YAML so the diagram can be embedded in other project docs (see In your docs below).

Examples

Three worked examples. Every image below is the <slug>.svg the editor generates on save — the same file you would embed in a project document.

Boot and idle

The diagram from the section above, using the palette on this page — shown here after Auto-layout, so the node positions differ from the hand-written layout block without changing what the diagram means. A linear boot path with one branch: calibration either succeeds into idle or fails straight to sleep.

Boot and idle: init through calibration into idle and sleep

Error recovery

A second diagram over the same palette — no new states, no new events, just different wiring. Here a failed calibration returns to init to retry instead of dropping to sleep.

version: "2.0"
name: "Error recovery"
description: "Retry calibration from init instead of falling through to sleep"

initial_state: init

transitions:
- from: init
to: calibration
event: init_success
- from: calibration
to: init
event: calibration_fail
- from: calibration
to: idle
event: calibration_success
- from: idle
to: sleep
event: idle_timeout

Error recovery: a failed calibration loops back to init

Keeping both diagrams side by side is the point of the split: the palette says what the firmware can do, and each diagram is one proposal for how it should behave. Reviewing them is comparing wiring, not vocabulary.

Over-the-air update

A different domain with its own palette — the update loop a connected device runs.

version: "2.0"
states:
- id: running
description: "Normal operation on the current firmware"
- id: checking
description: "Asking the server whether a newer build exists"
- id: downloading
description: "Fetching the new image"
- id: verifying
description: "Checking the downloaded image against its hash"
- id: applying
description: "Writing the image and rebooting into it"

events:
- id: poll_due
description: "The update check interval elapsed"
- id: update_offered
description: "Server returned a build newer than the running one"
- id: no_update
description: "Server returned nothing newer"
- id: download_done
description: "Image fully downloaded"
- id: checksum_ok
description: "Downloaded image matches its published hash"
- id: boot_ok
description: "Device came back up on the new image"
version: "2.0"
name: "Over-the-air update"
description: "Poll, download, verify, apply, confirm"

initial_state: running

transitions:
- from: running
to: checking
event: poll_due
- from: checking
to: downloading
event: update_offered
- from: checking
to: running
event: no_update
- from: downloading
to: verifying
event: download_done
- from: verifying
to: applying
event: checksum_ok
- from: applying
to: running
event: boot_ok

Over-the-air update: running, checking, downloading, verifying, applying

Note what the palette does not contain: there is no rollback state and no checksum_bad event, so no diagram in this project can wire one up. Adding either is a developer's call, made in palette.yaml — which is exactly the conversation the two-file split is meant to force.

Permissions

Access is controlled by the project role permission system, scoped by folder prefix. Roles you'd typically configure on a project:

RolePermission
Developerwrite on the state-machines/palette/ and state-machines/diagrams/ folders
Product managerwrite on the state-machines/diagrams/ folder only
Viewerread on either folder

The organization owner always has full access regardless of role.

Splitting the two folders is what lets you give a PM the right to edit diagrams without also giving them the keys to the vocabulary.

In the web UI

The State Machine page has two tabs that follow the two-role model:

  • Palette — a read-only listing of the states and events available on the project. Editing happens through the EmbedHub CLI; the tab shows the exact commands to pull, edit, and push palette.yaml.
  • Diagrams — a drag-and-drop canvas. Pick states from the sidebar to add them to the canvas, drag between handles to connect, and the editor prompts for the event from the palette. The Mark initial button sets the selected state as the entry point. Auto-layout runs a left-to-right graph layout. Read-only if you lack write permission on the diagrams folder.

The project page shows a State Machines section listing every diagram with a link into the editor, and inlines the most recent diagram as an image.

In your docs

Project documentation can embed a diagram by relative path — useful for design docs that should always show the current state machine. The file must use the .mdx extension; plain .md is handed to the browser as-is and the relative path won't resolve.

The onboarding flow is captured here:

![Onboarding](../state-machines/diagrams/onboarding.svg)

When the markdown viewer renders the page, the image resolves to the diagram's SVG from the same project. Update the diagram, refresh the doc, and the embedded image updates with it. See Markdown & MDX for the full set of MDX features.

From the CLI

Because the palette and diagrams are just files in your project, the standard file commands work:

# Pull the current palette and edit it locally
embedhub pull myorg/myproject state-machines/palette/palette.yaml
$EDITOR palette.yaml
embedhub push myorg/myproject state-machines/palette/palette.yaml

# List existing diagrams
embedhub list myorg/myproject --prefix state-machines/diagrams/

# Create or update a diagram from a local file
embedhub push myorg/myproject state-machines/diagrams/onboarding.yaml

See the CLI documentation for the full command reference.

For AI agents

An AI agent with the EmbedHub CLI installed can author both files the same way it would any other project file. The recommended flow:

# 1. Read the existing vocabulary so the agent knows what's available.
embedhub pull myorg/myproject state-machines/palette/palette.yaml

# 2. Generate or update a diagram referencing only ids that exist in the palette.
embedhub push myorg/myproject state-machines/diagrams/proposed-flow.yaml

The same prefix-based permissions described above apply: an agent operating with a project role that has write on state-machines/diagrams/* but not on state-machines/palette/* can freely add diagrams but cannot extend the vocabulary on its own — exactly the same boundary a human PM would hit.

When a diagram references states or events that aren't in the palette, the push is rejected with 422 and a list of the offending paths, so the agent gets immediate, structured feedback to fix the file.

The accompanying <slug>.svg rendered by the web editor is not required for the YAML to be valid — agents can push a diagram YAML without one and re-render later by opening the diagram in the web UI.