Anyone who has run agents on real work eventually hits the same wall. The output is good, but it is differently good each run. The formatting drifts. A number is transposed. A row appears that was never in the source. For a draft that a human reads, fine. For anything the business depends on, not fine, because you cannot tell a correct run from a plausible one without checking every line.
The usual workaround is to leave the product: push the exact step out to a separate automation tool so it runs the same way every time. You do not have to do that here. The workflow engine runs typed nodes with no model in the path at all.
The model reasons; the machine transcribes. Any step whose output is data — a parsed file, a converted record, a committed total — is a deterministic node. The model gets the work that needs judgment: classifying, matching, writing prose.
Steps
Find the step that has to be identical
Go through a workflow you already run and ask of each step: if this came out slightly different next Tuesday, would anyone notice, and would it matter? The steps where the answer is "no, and yes" are the ones to move.
Usually: totals, reconciliations, format conversions, anything committed to a system of record.
Know what the typed nodes are
| Type | Does |
|---|---|
command | Runs a shell command. Its stdout is the output, byte for byte. |
condition | Routes on a real test. Outgoing edges are labelled True and False. |
loop | Iterates an array, each pass with a fresh context. |
http | Makes a request and returns the response. |
wait | Pauses for a bounded time. |
A workflow becomes a graph the moment it has connections. Then the engine
owns the control flow: branches fork, joins wait, loops iterate, and none of that is the
model's decision.
Move the step
Before: one activity doing judgment and arithmetic together, which is where drift comes from.
After: the machine totals, the model writes.
The totals are now whatever the script printed. Not approximately that, exactly that. And a script is something you can test on its own, which a prompt is not.
Wire the graph
Each node reads the one before it with {{nodes.<id>}}.
If a command node prints JSON it is parsed, so the next node can address fields directly.
Make the last step affirmative
Between steps an evaluator can end a run. Two habits keep it honest: fewer and denser steps, each ending on something concrete, and a final step that returns the artefact — the path, the JSON, the summary. A last step reporting "nothing to do" can be promoted into ending the whole run.
Verify
- Run it twice on identical input and diff the two outputs; the deterministic parts must match exactly
- Feed it a row you know is wrong and confirm the script fails rather than the model smoothing it over
- Run the script by hand outside the workflow and confirm you get the same bytes
- Confirm the model never restates a figure it was given
When it goes wrong
The node output is not what the script printed
Something else is writing to stdout — a progress line, a warning. Send everything that is not the result to stderr.
The next node cannot read the previous one's fields
Output was not valid JSON, so it arrived as a plain string. Print strict JSON and nothing else.
The model recomputes anyway
Say it in the step: use these figures exactly, do not recompute. Models are helpful by default and will check arithmetic you did not ask them to check.