Skip to main content
loomcycle
§ story · loomboard canvas · v1.105

A canvas for state-machine workflows.

A multi-agent workflow in loomcycle is a state machine. It has an input, an output, and a graph of transitions between states where each state is a sub-agent run. You can describe a state machine in YAML. Loomcycle has supported that for a long time, and the previous post covered how a whole workflow now ships as one YAML file.

You can also describe a state machine by drawing it. That is the subject of this post.

Why both? A YAML team is right for the CI pipeline, the diff review, the operator who already thinks in text. A canvas team is right for the first draft, for showing a colleague what the workflow does without them having to read it, for debugging a stuck walk by pointing at the state that is red.

The loomboard canvas for teams shipped in three arcs across September and October. Here is what each added, and what the whole thing does now.

The short version. The teams canvas renders a team definition as nodes and edges, highlights states as a walk progresses, opens a side pane with the sub-agent's conversation on click, and lets you abort a walk from the canvas. Channel and variable nodes drag-to-inject onto states. An Input node starts a walk from a form; an End node shows the output chunk. A tri-view puts the canvas and the JSON side by side with cursor sync (click a node, the JSON jumps to its definition; click a JSON path, the canvas selects the node). New team / Save as new / Import / Copy / Download for standard file ops. A Check button calls TeamDef op=verify without saving and surfaces the refused | unrunnable | advisory findings at the JSON path at fault.

States as nodes, transitions as edges

The first thing the canvas renders is the state machine itself. Every state in the team definition becomes a node. Every transition becomes an edge. The layout algorithm places entry states on the left, terminal states on the right, with branches flowing top-to-bottom where the state machine branches.

flowchart LR
  INPUT([Input: ticket])
  TRIAGE[["triage
sub-agent: priority-decider"]] ROUTE{"route by priority"} ESCALATE[["escalate
sub-agent: escalation-writer"]] AUTO[["auto-resolve
sub-agent: resolver"]] END([End: response]) INPUT --> TRIAGE TRIAGE --> ROUTE ROUTE -->|urgent| ESCALATE ROUTE -->|normal| AUTO ESCALATE --> END AUTO --> END style INPUT fill:#e8f4ff,stroke:#4a90e2 style END fill:#e8ffef,stroke:#4a9e60 style ROUTE fill:#fff8e0,stroke:#c99a3c
A simple triage team as the canvas renders it. State nodes carry their sub-agent name; routing nodes carry their condition expression; Input and End nodes are the entry and exit points.

Click a state node and a side pane opens with the state's full definition: its sub-agent, its prompt template, its capture rule, its timeout, its retry policy. Edit any of them inline. The canvas and the JSON update together.

Click an edge and the condition that gates the transition opens for editing. A routing node with four branches has four edges to four target states; each edge carries its own condition.

Live run visualization

The second thing the canvas renders is what the state machine is doing right now. A team walk that is running shows up on the canvas with the active state highlighted. As the walk progresses, each state transitions through the full set: idle, running, succeeded, failed. The canvas colours them accordingly.

Click a running state and the side pane changes shape: instead of the state's definition, you see the sub-agent's conversation, streaming live. Every tool call, every tool result, every model turn. The same view the operator console shows for a standalone run, now attached to the state that produced it.

An abort control at the top of the canvas stops a walk mid-flight. The runtime cancels the current state's sub-agent (sending a soft stop first, a hard kill after a short grace period), records the walk as cancelled, and the canvas colours the state red with a reason tooltip.

Channels and variables as nodes

Channels and variables used to be invisible on the canvas. A state that published to a channel had the channel named in its definition pane; a state that read a variable had the variable in its template. You could see what a state did; you could not see how channels and variables tied the whole team together.

Second arc added channel nodes and variable nodes to the canvas. A channel node is a small round node that shows the channel's name and the number of messages waiting. A variable node is a small square node that shows the variable's name and its current value.

Drag-to-inject. Drop a channel node onto a state node, and the state binds to that channel (either as subscriber or publisher, depending on which handle you drop onto). Drop a variable node onto a state node, and the state's prompt template gets the variable interpolated at the cursor position. No typing. The YAML updates underneath.

Input and End nodes. An Input node is a special variable-shaped node that starts a walk. It carries the declared variables the team expects; dragging it to the canvas and clicking Run opens a form with the declared variables as fields, filled in once per start. The End node shows the terminal output chunk when a walk completes. If the team's output is structured, the End node renders the structure; if it is text, it renders the text.

Canvas, JSON, and the Check button

The third arc is the one that makes the canvas useful for production work. It added a tri-view (Canvas / JSON / Split) and standard file operations, plus a one-shot verify.

Split view with cursor sync. Canvas on the left, JSON on the right. Click a node in the canvas, the JSON jumps to its definition and highlights the block. Click a JSON path, the canvas selects the corresponding node. Edit either side; the other updates. The underlying model is the JSON; the canvas is a view of it. Changes you make in the canvas round-trip through the JSON cleanly, so a team authored on the canvas and reviewed in diff reads as the same team.

Standard file ops. New team starts an empty definition. Save as new creates a fresh team in the current tenant. Import takes a JSON file (or paste); the canvas renders it. Copy copies the current JSON to the clipboard. Download writes it to a .json file.

The Check button calls TeamDef op=verify on the current (unsaved) definition. Response is severity: refused | unrunnable | advisory with the JSON path at fault. The canvas surfaces each finding next to its JSON cursor and places a badge on the offending node. A refused team cannot save; an unrunnable team can save but will not start; an advisory team saves and starts with the advisory as a warning.

The Check button is what makes "edit on the canvas, save when ready" a reliable loop. You never have to leave the canvas to find out whether what you have is valid.

What this does not do yet

Three limits worth naming.

No multi-user editing. Two operators editing the same team on their own canvases will produce a last-write-wins conflict at save time, because the canvas has no awareness of other editors. The usual pattern for now is to use the Save-as-new flow for anything collaborative and reconcile later. Multi-user editing with presence is on the roadmap.

Live run visualization requires the operator token. A viewer that only has a read-only tenant token sees the team definition but not a live walk's progress or sub-agent conversations. The live stream reads from the same channels that the operator console reads; access is scoped to the operator identity.

Channel node messages shown are counts, not previews. A channel node shows "17 messages waiting" but not the messages themselves. Clicking the node opens the channel's own side pane for the preview. The reason is prudent: channel contents may be sensitive to the walk's own privacy settings, so the canvas does not surface them without an explicit click.

What this makes possible

A workflow prototyped on the canvas by someone who thinks in diagrams can be reviewed in JSON by someone who thinks in text; both halves see the same definition. A stuck walk opens on the canvas with the exact state highlighted, click once for the sub-agent's conversation, click abort if it is lost. A new team imports from a shared .json, runs with a form, and ships without anyone ever opening a YAML editor.

The canvas and the self-contained team file arrived in the same arc because they are the same work from two sides. The self-contained file (see the teams post) is what makes the canvas's Save, Download, and Import operations round-trip losslessly; the canvas is what makes the file approachable to a reader who does not already know the YAML schema.

What comes next

Multi-user presence on the canvas and a replica-safe team walk (so a restart does not end a walk) are the two significant remaining gaps. On the primitive side, the Decision tool opens up team-level routing nodes that read a probability-ranked list of next states instead of a hand-written condition expression. On the orchestration side, detached sub-agents make a canvas-drawn fan-out viable without the parent blocking.