Agents should write workflows, not improvise them
William Thompson (@iteratingself)
A customer asks for a refund. The agent looks up the order, checks refund eligibility, issues the credit through the billing system, closes the support ticket, and posts a confirmation to the customer’s chat thread. Five actions across four systems: CRM, billing, ticketing, chat. Not one of them was decided in advance. The agent worked out the order at inference time, call by call, while it was running. (Compounding per-step failure rates are their own problem. Chain enough individually reliable calls and some run fails partway through, on a schedule you can predict. This piece is about what happens once a step fails, not how often one does.)
That is the default shape of “give an agent a business action to perform.” And “the agent decided the order” is doing more work in that sentence than it looks like.
The agent is the orchestrator
Hand an agent a catalog of API primitives, over MCP or something like it, and let it chain the calls itself. You have made an architectural choice, whether or not anyone meant to make one: the agent is the orchestrator. Four things follow.
- Control flow is emergent. What runs, in what order, whether a step can be skipped this time: all decided per run, by inference, living only in that run’s context. The same request can take a different path tomorrow. Nobody chose that. It is just what the model did given what it saw.
- There is nothing to review before the run. The “plan” exists only afterward, reconstructed from a trace. You cannot sign off on a workflow that doesn’t exist until it is already executing.
- Failure recovery is improvised. The easy version of this argument says the model isn’t smart enough to figure out what it already did. The easy version is wrong. A frontier model is usually good at this. Point it at its own transcript after a failed step and it will typically read the resource IDs out correctly, reason about what succeeded, and improvise a sensible recovery. The problem is the word “usually.” Recovery correctness becomes a probabilistic property of one run, decided fresh each time, instead of a declared property of the workflow. Best-effort inference has no floor under it, and it runs on the failure tail, the one place nobody is watching closely enough to catch it being wrong. Three facts make this worse. First, the reversibility semantics were never in the agent’s context. Whether a captured payment is voidable or only refundable, whether a CRM delete cascades to related records, that a chat message cannot be unsent: none of that is a fact the agent looked up. It is world knowledge the model improvises on the spot, dressed up as an answer. Second, the context that knows what happened is ephemeral. The session ends. Three weeks later someone in billing notices the credit and the ticket-close don’t line up, and a different session, or a human with no transcript, has to work out what actually happened. Third, even a correct improvised recovery leaves nothing behind. It is a fresh performance every time, and nine correct performances tell you nothing about the tenth.
- The audit trail is best-effort exhaust. Whatever got logged got logged because a framework happened to catch it on the way past, not because anything was designed to make the record exist.
None of this is a knock on exploratory work. Let an agent figure out its own tool sequence when you are prototyping, when a human watches every step, and when getting it wrong costs a retry and nothing else. It is a different question when the action is one your business needs to run one specific way, every time, and someone downstream (a regulator, an auditor, your own billing team) will ask what happened and expect a better answer than “probably that.”
What a workflow needs instead of an orchestrator
I spent years building the equivalent guardrail for where cloud workloads were allowed to run before I built one for what an agent may do to a customer record. The shape of the problem barely changes: emergent behavior decided at runtime, versus a boundary decided in advance and enforced the same way every time. The fix generalizes the same way, and none of it requires giving up what makes agentic tooling useful in the first place.
- Write the workflow down. The sequence of primitive calls (get the order, check eligibility, issue the credit, close the ticket, post the confirmation) becomes a declared, versioned, immutable composition, not something re-derived per run. The agent stops orchestrating and becomes a caller. It picks a composition and supplies typed input. It can still speak MCP; the interface doesn’t have to change. But one tool call is now one whole reviewed workflow, not a raw primitive the agent is trusted to sequence correctly.
- The reviewable artifact exists before the run. The fully resolved execution plan, every step in declared order with its declared undo, can be computed with zero side effects before a single call goes out. That artifact cannot exist when the plan is invented at runtime. You cannot review a plan that doesn’t exist yet.
- Failure is a contract, not archaeology. When a step fails, what comes back is the failed step with the provider’s real error, the completed steps with the resources they actually created, whether the run can resume, and an unwind plan that distinguishes steps to clean up regardless of what happens next from steps safe to leave alone if someone resumes the run later. A step with no real compensating action, a sent chat message or a captured payment, is declared
undo: none. Not omitted because it is inconvenient. Declared, because that is the true shape of the operation, and the caller needs to know it before running the step, not after. - The audit trail is the system of record, not exhaust. The record is not bolted on by an observability tool that happened to be watching. If the record cannot be written, the action does not proceed. Log-or-halt, not log-if-convenient. Regulators have started saying this plainly: the EU AI Act’s Article 12 requires automatic event logging as a designed-in property of a high-risk system, not an integration afterthought.
The obvious objection: writing workflows down is what we were escaping. Nobody adopted agents to go back to hand-coding glue for every provider pair. The answer is not to take the model out of the loop. It is to move the model to the right end of it. An agent is genuinely good at reading API docs and composing calls into a working sequence, so let it do that at authoring time. The agent drafts the composition, validates it, and proposes it. A human reviews the proposal and signs off. The reviewed artifact is what runs, every time after. The improvisation still happens, once, where it is cheap to inspect and cheap to reject. What changes is that it stops happening on every run, in production, against live customer records.
Get Iteratingself’s stories in your inbox
Join Medium for free to get updates from this writer.
Chaining call-by-call through an agent is also slow and expensive. Every step is a separate inference pass, and every raw API response gets carried forward in context whether the agent needs it again or not. Server-side execution of the declared workflow fixes that too. It is real money, but it is the second reason to do this, not the first.
Where I’ve been building this
The general shape above is what I have been building, concretely, as Actum (open source). A composition is exactly the object described in the last section: a declared, versioned sequence of API calls, its full execution plan resolvable with zero side effects before it runs, its failures returned as a structured contract instead of a transcript someone has to comb through, its audit trail written or the action doesn’t happen. The authoring flow is built in as well: an agent can draft and propose a composition, but the sign-off that lets it into a governed namespace is reserved for a human, and the proposer can never be its own approver.
Before you wire an agent to a catalog of API primitives and let it figure out the order, ask the plainer question: if this workflow has to run the same way every time, why is a model deciding the order fresh on every run? If the honest answer is that it doesn’t have to, because this is prototyping or a human is watching every step, the agent-as-orchestrator shape is fine. Keep it. If the honest answer is that it does, the fix is not removing the model. It is moving the model: let it write the workflow once, get the workflow reviewed, and run the reviewed thing every time after.






