State machines

Draw how a component behaves over time

An architecture diagram shows what talks to what. A state machine shows what one component does as things happen to it: an order that is pending, paid, shipped or cancelled. On the States tab you draw that as a statechart, link its transitions to the events and endpoints on your API tab, and keep it in the same file as the rest of the diagram.

What it is

A statechart is a set of states and the transitions between them. A transition fires when something happens (its trigger), can be held back by a guard, and can run actions. A component can own any number of machines, and every machine belongs to one component on the Architecture tab. A statechart adds nesting and concurrency to a plain state diagram, with five kinds of state.

  • Atomic: a state with nothing inside it.
  • Compound (Group in the palette): a state that holds child states, one of them the initial child.
  • Parallel: a state whose regions are all active at once, for example shipping and invoicing. Each region is a compound state, and a parallel state needs at least two.
  • History: remembers the child a compound state was last in, so a transition into it resumes there. It is shallow (the direct child) or deep (all the way down).
  • Final: the machine, or the region, is done.

States can have entry and exit actions, which are plain text such as reserveStock. A machine always has at least one state; a new machine starts with one called Idle.

The States tab

Open the States tab next to Network and Notes. You build on the canvas and set up the details in a panel on the right. There are three parts.

  1. The left bar: the components that can have a state machine (services, functions, clients, workflows, auth and realtime components), each with its icon and how many machines it has, and a search box that finds a component by its name or by one of its machines' names. A component that already owns a machine is listed even if it is another kind. Click one to see its machines, each with how many states it has. The ⋯ on a machine renames it, edits its description or deletes it. New machine and Import are here too. Under them, Add to canvas is the palette: State, Group, Parallel, Final, History and Start.
  2. The canvas, with a bar above it that shows the machine's name and its component, the warnings count, and Auto-layout. Import and export are in the editor toolbar's Import and Export menus, under This tab.
  3. The right panel, which only opens while a state or an arrow is selected. Click empty canvas to close it.

With nothing picked, the tab opens the first component that owns a machine, inside its first machine. A link from another tab (an event's consumers, or Across views) opens the tab inside the linked machine. The arrow at the top of the left bar collapses it to a narrow rail, as on the other tabs.

Build on the canvas

  • Add a state by clicking a palette tile, which adds it in the middle of the part of the canvas you can see, or by dragging the tile where you want it. A new state opens with its name ready to type. Drop a tile on a group or on a region of a parallel state to put it inside. A parallel state's own area only takes a Group, which becomes a new region. A new Parallel comes with two regions.
  • Connect states with the blue + on a state's right edge, which shows when you hover or select the state. Drag it onto another state to draw an arrow. Drag it onto an empty spot to add a new state there, already connected, inside the group or region under the cursor if there is one. One Undo removes both. A new arrow starts as an event called event and is selected, so you can set its trigger in the right panel.
  • Add inside a group with the + in its header. On a parallel state the + adds a region, and each region has its own + for states.
  • Rename a state by double-clicking it, or from its ⋯ menu. Enter keeps the name and Escape cancels.
  • Set what happens on entry or exit from the toolbar above a hovered or selected state: + entry and + exit add an action, which shows as a chip under the state. Click a chip to edit it, or its x to remove it.
  • Start here, in the same toolbar, makes the state the initial state of its machine, or of its group. It is not offered for a history state or for a region of a parallel state. Dropping the Start tile on a state does the same, and so does clicking the tile while a state is selected. Clicking History adds it inside the selected group, so select a group first.
  • The toolbar's ⋯ menu has Change kind, Move into… (a list of the groups the state can go in, and Top level), History type for a history state, Add transition to…, Rename and Delete. It only offers choices that keep the machine valid.
  • Nest by dragging: drop a state into a group or into a region of a parallel state to move it inside, or drop it outside to move it out. If that would leave a group without an initial child, Modudraft picks another one in the same edit, and one Undo puts everything back.
  • Collapse a group with the arrow in its header to hide what is inside. Collapse is only for the session you are in and is not saved in the file.
  • Auto-layout arranges the whole machine again, ignoring the positions you saved and any collapse, and saves the new positions as one undo step. Without it, saved positions are kept and new states are placed to their right.
  • Delete removes the selected state or arrow. Deleting a state also deletes the states inside it and the arrows that touch them.

The right panel

Select an arrow to set its Trigger (Event, API event, Endpoint, Timer or Always), a Guard, Actions and a Description, and to delete it. Select a state to edit its Description. Both show the warnings that are about the item, with their fixes. Choosing no target makes a transition internal: it runs its actions and stays in the state.

On a phone

On a phone you can look at a machine, pan and zoom, but you do not drag on the canvas. The left bar becomes a sheet at the bottom of the screen: tap its handle to open it. Tapping a palette tile adds that state at the top level. Select a state to see its toolbar, where Move into… nests it and Add transition to… draws an arrow, the same as dragging on a desktop. The right panel opens as a sheet when you select something.

People who can only view the diagram (a share link or an embed) can still use the lists, the warnings count and the Export menu, but there is no palette and nothing to add or import. On a locked diagram the palette is turned off, Auto-layout is hidden and the import is turned off, until you unlock it.

A transition has one trigger.

  • Event: a name you type, such as stock.reserved. It is plain text, up to 200 characters.
  • API event: linked to an event on the API tab, such as payment.succeeded. Only events can be picked.
  • Endpoint: linked to an HTTP or other non-event endpoint on the API tab, such as POST /orders/{id}/cancel.
  • Timer: fires after a delay, which you enter in milliseconds. It shows as a duration, for example "after 30 min".
  • Always: fires as soon as the state is entered and its guard, if it has one, passes.

A linked trigger follows the endpoint: rename the endpoint and the transition shows the new name. If the endpoint is deleted, or is the wrong kind for the trigger, the transition keeps its place and shows Link removed until you pick another one or delete it.

Events on the API tab can list consumers: the components that receive the event. When a machine reacts to an event whose consumers do not include its component, the machine's warnings say so and offer Add as consumer, which fixes it in one click. The component that publishes the event does not need to be one of its own consumers.

Warnings and rules

Some problems stop a file from saving, because the machine would not make sense. Others only warn.

Blocks saving

  • A machine with no states, or whose initial state is not one of its top-level states.
  • A parent that is missing, is not a compound or parallel state, or forms a loop.
  • A compound state with children and no initial child, or an initial child set on a state that is not compound.
  • A parallel state with fewer than two regions, or a region that is not a compound state.
  • A history state outside a compound state, or a history type on a state that is not a history state.
  • A transition whose source or target is not a state of the same machine.
  • An owner that is missing or is not a component, and consumers on an endpoint that is not an event.
  • Text over the caps: names, guards and each action up to 200 characters, descriptions up to 2000. A file that already has longer text is cut to the cap, ending in "...", when it opens.

Edits in the editor that would break a rule are refused with the reason. A file that was already broken is repaired when it opens and you see a notice that says so.

Only warns

  • Link removed: the linked endpoint is gone or is the wrong kind.
  • Not a consumer: the machine reacts to an event whose consumers do not include its component.
  • Unreachable: no path of transitions leads to a state from the initial state.
  • A consumer no longer exists: an event lists a component that has been deleted.

Machines, states and transitions count toward the limit of 5,000 items in a tab.

Import and export

Import in the left bar, or Import then State machine… in the editor toolbar, adds a new machine to that component. Paste text; Modudraft detects the format, shows a preview, and lists what it did not import. You can rename the machine before you add it.

  • Mermaid: stateDiagram-v2 and stateDiagram, with composite states, -- regions, [*] start and end, transition labels, notes, aliases and entry or exit lines. A stereotype such as <<choice>> is imported as a plain state, and lines Modudraft does not understand are listed in the report.
  • XState: a version 5 machine config, as JSON or as a JavaScript object. The text is read as data and is never run: nothing in it is evaluated, so functions, template strings, spreads, computed keys and getters are not executed. They are skipped and appear in the report. Also skipped and reported: context, invoke, inline actions and guards, named delays, multiple targets and any key Modudraft has no place for.
  • The Architecture tab's Import Mermaid dialog does not take state diagrams. Pasting one there tells you to import it from the States tab.
  • An imported event name that is exactly the name of one API endpoint becomes a link to it.

Export then State machine… in the editor toolbar gives you the selected machine as Mermaid, XState JSON or PlantUML, to copy or download. Mermaid and XState export import back to the same names and labels. PlantUML is export only. A linked trigger exports as the endpoint's name. In Mermaid, the parts that have no direct syntax are written like this:

FeatureIn Mermaid
Entry and exit actionsS : entry / a, b lines
Internal transitionS : on label
Parallel regionsNamed composite states separated by --
History stateA state named H (shallow) or H* (deep)
Final stateF --> [*]

A transition label reads event [guard] / action1, action2. The slash needs a space on both sides, so a name like POST /orders/{id}/cancel stays whole. An empty label is an always transition, and after 1000ms is a timer.

AsyncAPI

Consumers on API events feed the AsyncAPI export. On the API tab, open Export in the toolbar and choose AsyncAPI… (it appears once the diagram has an event) to get a document in YAML or JSON. It is AsyncAPI 3.0.0 and holds your event endpoints only. Each topic becomes a channel with its payload message, each publishing component gets a send operation, and each consumer you listed gets a receive operation. Endpoints that are not events are left out; use the OpenAPI export for those.

That is why the missing-consumer warning matters: a machine that reacts to payment.succeeded is a consumer of it, and the export says so once you add it.

With an agent (MCP)

Agents work with machines through the states toolset of the MCP server. The quickest way is apply_state_machine: one nested JSON shape builds the whole machine in a single write, all or nothing. It creates a new machine, or replaces one when you pass machine_id.

{
  "diagram_id": "abc123",
  "node": "Orders service",
  "machine": {
    "name": "Order",
    "initial": "Pending",
    "states": [
      {
        "name": "Pending",
        "entry": [
          "startPaymentTimer"
        ],
        "on": [
          {
            "api_event": "payment.succeeded",
            "target": "Paid"
          },
          {
            "endpoint": "POST /orders/{id}/cancel",
            "target": "Cancelled"
          },
          {
            "after_ms": 1800000,
            "target": "Cancelled"
          }
        ]
      },
      {
        "name": "Paid",
        "on": [
          {
            "event": "parcel.shipped",
            "target": "Shipped"
          }
        ]
      },
      {
        "name": "Shipped",
        "kind": "final"
      },
      {
        "name": "Cancelled",
        "kind": "final"
      }
    ]
  }
}
  • Use api_event and endpoint to link a transition to the API tab. A plain event name stays plain text unless you import it from text.
  • Targets are a dotted path from the machine root (Fulfilment.Shipping.Shipped) or a name that is unique in the machine. An ambiguous target, a repeated sibling name or a parallel state with one region is refused with the state path and the fix, and nothing is written.
  • After the write, get_state_machine returns the machine with its warnings. Fix them with set_event_consumers, add_transition or update_transition, then read it again.
  • A very large machine is refused with a message to split it, so a failed write never leaves half a machine behind.
  • export_state_machine returns Mermaid, XState or PlantUML text, and export_asyncapi in the api toolset returns the AsyncAPI document.

Edits are sent as small changes, so they do not overwrite what someone is editing in the web app at the same time. If a person deletes a state while the agent is adding a transition into it, the agent is told the state is gone and nothing is written.

Update the MCP server to 0.25.0 or later. Older clients cannot edit machines, and on a diagram that has state machines they are refused when a write would leave a machine without its owner (deleting or replacing a component that owns one), and when they clone the diagram.

Try it

The E-commerce checkout example on the home page includes a States tab: open it to see the Order machine on the Orders service. It waits for payment, runs shipping and invoicing in parallel, and handles cancel and refund paths, a payment timer and a history state. It reacts to payment.succeeded, which Payments publishes and Orders consumes. Click a transition to see its trigger. Or open it in the demo, no sign-up needed.