A conversation that worked once is not a routine. A workflow is a declared sequence: the same steps, in the same order, with a trigger that starts it and a record of what happened. It is also where a step that must call a tool can be made to fail rather than quietly report success.
The shape
Workflows live in an employee's agent.json, keyed by name, each with a trigger
and a list of activities.
That is the sequential form: each activity runs after the last, and its output becomes context for the next. Every activity is model-executed.
The two kinds of step
The moment a workflow has connections, it runs on the graph executor instead, and
this is where it stops being a prompt chain. The engine owns the control flow, and typed nodes
execute with no model in the path at all.
| Node | What it does |
|---|---|
command | Runs a shell command. Its stdout is the node output, byte for byte. |
condition | A real test. Outgoing edges labelled True and False. |
loop | Iterates an array, each pass with a fresh context. |
http | Makes a request, returns the response. |
wait | Pauses for a bounded time, up to an hour. |
The model reasons; the machine transcribes. Any step whose output is data — a parsed file, a converted record, a committed total — belongs in a typed node. The model gets the work that needs judgment: classifying, matching, writing. Asking a model to reproduce data exactly is a hallucination trap even with precise instructions.
Wiring the graph
__trigger__ is where the run enters and __emit__ is where it
announces it finished, which is what another employee listens for. Nodes read each other with {{nodes.<id>}}, and a node that printed JSON is parsed,
so the next one can address its fields.
Large inputs
For anything bigger than a few dozen records, do not put the records in the conversation. Have a command node write chunk files and print only pointers, loop over the pointers, and let each pass read its own file. The data enters as tool output rather than as context, and the conversation never grows with the input.
Triggers
| Type | Fires |
|---|---|
schedule | On a cron expression |
heartbeat | Every interval, optionally only inside a window |
event | When a named source emits |
watch | When a plugin streams a change |
folder | When matching files change on disk |
manual | When you ask |
heartbeat with a window is the one people miss. It runs every interval but only
during the hours you name, so a business-hours job is not burning through the night.
Two authoring rules
Between steps the engine evaluates progress and can end a run. Two habits avoid ending one by accident.
- Fewer, denser steps, each finishing on something concrete. Put conditional sub-actions inside a step rather than making them their own.
- End affirmatively. The last step returns the artefact — the path, the JSON, the summary. A final step that says "nothing to do" can be promoted into ending the whole run.
Keep the sum of activity token budgets at or below the run total. Inline workflows are not checked at load time, but both limits are enforced while running, so a mismatch surfaces as a truncated run rather than a clear error.
Check it
- Run it twice on the same input and diff; the typed nodes must match exactly
- Break a command node deliberately and confirm the run fails rather than continuing
- Confirm the last step returns something, on the path where there is nothing to do