Deterministic engine, non-deterministic model
The usual framing is wrong, and the correct composition is now a shipped contract rather than an argument.
The common claim: workflows require determinism, agents are non-deterministic, therefore agents cannot be durable workflows. That does not follow.
Determinism is required of the orchestration calls, not of the values. A model call inside an activity produces a value written to history exactly once. On replay the workflow does not call the model again — it reads the recorded response. The agent's control flow, branching on that response, replays identically.
workflow (deterministic, replayable):
loop:
resp = step(call_model, messages) # non-determinism recorded here
if resp.is_final: return resp
for call in resp.tool_calls:
result = step(run_tool, call,
key=deterministic_key(run_id, turn, call.index))
messages.append(result)This is no longer a design argument. One durable engine's official integration with a popular graph framework makes it a requirement: every node must declare whether it executes as an activity or inside the workflow, and the plugin errors if you omit it. Use activity for "LLM calls, HTTP requests, database queries, or any I/O." A node running in the workflow "must not make network calls, use random, read the system clock, or do file I/O."
And the detail that shows the composition properly: when you run a graph this way you use an in-memory checkpointer, because the durable engine handles durability and third-party checkpointers are not needed. The engine replaces the framework's persistence rather than layering on it. If you are running both, one of them is doing nothing.
What does letting the model direct control flow cost you that is hardest to recover?
The rest of this lesson unlocks when you commit above.