{"course":{"slug":"agents-and-automation","title":"Agents and Automation","url":"https://addaly.com/learn/agents-and-automation","level":"intermediate","track":"build"},"lesson":{"slug":"state-machines-over-loops","title":"When the loop should be a graph","url":"https://addaly.com/learn/agents-and-automation/state-machines-over-loops","position":33,"of":72,"minutes":9,"answer":"Splitting an agent into named states lets each state carry only the tools it needs, which is real containment rather than an instruction not to use the dangerous one.","keyIdea":"Splitting an agent into named states lets each state carry only the tools it needs, which is real containment rather than an instruction not to use the dangerous one."},"body":{"format":"markdown","text":"## Choice is a cost as well as a feature\n\nA free-running loop gives the model a decision at every step. That is exactly right when nobody knows the path in advance, and exactly wrong when everybody does. A refund process has been the same five steps for eleven years. Asking a model to rediscover them forty times a day is paying for creativity in a place where creativity is a defect.\n\nThe alternative is a state machine: named states, defined transitions, and a model call only where a genuine judgement is required.\n\n```\nverify_order ──> check_policy ──> decide ──┬──> issue_refund ──> notify\n                                            ├──> escalate ─────> notify\n                                            └──> reject ───────> notify\n```\n\n`verify_order` is a database lookup, no model. `check_policy` reads the policy text against the order and is a model call. `decide` is a model call returning one of three enum values. `issue_refund` is code with a limit. `notify` is a template. One agent turned into three model calls and four functions.\n\n## The containment argument\n\nThe strongest reason is not tidiness, it is **tool exposure**.\n\nIn a free loop, every tool is available on every turn. The refund tool sits in the tool list while the agent is reading an email, and the only thing standing between an injected instruction and a payment is an instruction in the prompt asking it not to.\n\nIn a state machine, each state carries its own tool list. The `notify` state has no refund tool, not a forbidden one, an absent one. The `check_policy` state can read and cannot write. A prompt injection that arrives while reading the customer's message lands in a state where the dangerous capability does not exist, and the class of attack does not apply there. This is the difference between a rule and a boundary, and it is available to you for the price of splitting one prompt into three.\n\n## The other benefits, briefly\n\n- **Enumerable paths.** Seven states and eleven transitions can be listed, and each tested. A twenty-turn free loop has no path list at all.\n- **Resumability.** A run that dies in `check_policy` restarts at `check_policy`, because the state is a value you stored, not a position in a transcript.\n- **Cost.** Each state gets a short prompt and only the context it needs. Three short calls beat one twenty-turn transcript, and the arithmetic from the context-window lesson makes that difference large.\n- **Small prompts.** Each state's instruction is a paragraph about one decision, which is far easier to write, test and ablate than a page trying to govern everything at once.\n\n## Keep loops where they belong\n\nInside a state, when the work is exploratory (searching until something is found, retrying a flaky lookup, reading files until a question is answered) use a loop with its own budget. The graph is the outer structure; loops live inside individual nodes. Most good agent systems are shaped like this rather than being purely one or the other.\n\n## What to build it with\n\nA dictionary of handler functions and `while state != \"done\"` is a complete implementation and stays debuggable at 3am:\n\n```python\nstate, ctx = \"verify_order\", {\"order_id\": order_id}\nwhile state != \"done\":\n    state, ctx = HANDLERS[state](ctx)\n    log(run_id, state, ctx)\n```\n\n**LangGraph** gives you this with persistence and streaming attached. **Temporal** gives durable execution, meaning a workflow survives the machine dying mid-step, which matters when a state takes ten minutes and calls a payment API. Both are real tools with real costs: a framework you cannot read is a debugging problem you have deferred, not avoided. Start with the dictionary; move when you can name what you need.\n\n## The limitation\n\nA state machine encodes a process you already understand. If nobody can draw the process, you cannot draw the graph, and forcing one prematurely produces a rigid system that handles the twelve cases somebody imagined and breaks on the thirteenth.\n\nSo the sequence that works is: prototype with a free loop, run it on real inputs, then **read fifty traces and extract the graph the traces already show you**. The states will be obvious by then, and so will the two transitions you would never have predicted. Building the loop first is not wasted work; it is how the map gets drawn."},"check":{"question":"A team converts a free-running refund agent into five explicit states. Beyond being easier to test, why does this reduce the damage a prompt injection in a customer email can do?","options":[{"text":"Each state exposes only the tools it needs, so the refund tool is absent, not merely forbidden, while the agent is reading customer text.","correct":true},{"text":"With fewer choices at each step, the model is more accurate and less likely to be misled.","correct":false,"tempting":"Narrower choice does tend to improve accuracy, but accuracy is not containment: a more accurate model still holds the dangerous tool."},{"text":"The transcript in each state is shorter, so there is less room for injected text to influence the outcome.","correct":false,"tempting":"Shorter context reduces dilution effects, but a single injected sentence in a short prompt is if anything more influential, not less."},{"text":"The run passes through fewer total steps, so there are fewer opportunities for an error to compound.","correct":false,"tempting":"Confuses compounding error with an adversarial input; one hostile message needs only one opportunity to matter."}]},"prev":{"slug":"budgets-and-stopping-rules","title":"Budgets belong in the harness, not the prompt","url":"https://addaly.com/learn/agents-and-automation/budgets-and-stopping-rules"},"next":{"slug":"retries-and-idempotency","title":"Retries, timeouts and doing it twice","url":"https://addaly.com/learn/agents-and-automation/retries-and-idempotency"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'When the loop should be a graph', in Agents and Automation. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/agents-and-automation/state-machines-over-loops (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"When the loop should be a graph","courseTitle":"Agents and Automation","module":{"index":4,"slug":"making-it-reliable","title":"Surviving real data"},"answer":null,"keyIdea":"Splitting an agent into named states lets each state carry only the tools it needs, which is real containment rather than an instruction not to use the dangerous one.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"choice-is-a-cost-as-well-as-a-feature","runs":[{"text":"Choice is a cost as well as a feature"}]},{"t":"p","runs":[{"text":"A free-running loop gives the model a decision at every step. That is exactly right when nobody knows the path in advance, and exactly wrong when everybody does. A refund process has been the same five steps for eleven years. Asking a model to rediscover them forty times a day is paying for creativity in a place where creativity is a defect."}]},{"t":"p","runs":[{"text":"The alternative is a state machine: named states, defined transitions, and a model call only where a genuine judgement is required."}]},{"t":"code","lang":"","label":"Text","text":"verify_order ──> check_policy ──> decide ──┬──> issue_refund ──> notify\n                                            ├──> escalate ─────> notify\n                                            └──> reject ───────> notify"},{"t":"p","runs":[{"text":"verify_order","code":true},{"text":" is a database lookup, no model. "},{"text":"check_policy","code":true},{"text":" reads the policy text against the order and is a model call. "},{"text":"decide","code":true},{"text":" is a model call returning one of three enum values. "},{"text":"issue_refund","code":true},{"text":" is code with a limit. "},{"text":"notify","code":true},{"text":" is a template. One agent turned into three model calls and four functions."}]},{"t":"h2","id":"the-containment-argument","runs":[{"text":"The containment argument"}]},{"t":"p","runs":[{"text":"The strongest reason is not tidiness, it is "},{"text":"tool exposure","b":true},{"text":"."}]},{"t":"p","runs":[{"text":"In a free loop, every tool is available on every turn. The refund tool sits in the tool list while the agent is reading an email, and the only thing standing between an injected instruction and a payment is an instruction in the prompt asking it not to."}]},{"t":"p","runs":[{"text":"In a state machine, each state carries its own tool list. The "},{"text":"notify","code":true},{"text":" state has no refund tool, not a forbidden one, an absent one. The "},{"text":"check_policy","code":true},{"text":" state can read and cannot write. A prompt injection that arrives while reading the customer’s message lands in a state where the dangerous capability does not exist, and the class of attack does not apply there. This is the difference between a rule and a boundary, and it is available to you for the price of splitting one prompt into three."}]},{"t":"h2","id":"the-other-benefits-briefly","runs":[{"text":"The other benefits, briefly"}]},{"t":"ul","items":[[{"text":"Enumerable paths.","b":true},{"text":" Seven states and eleven transitions can be listed, and each tested. A twenty-turn free loop has no path list at all."}],[{"text":"Resumability.","b":true},{"text":" A run that dies in "},{"text":"check_policy","code":true},{"text":" restarts at "},{"text":"check_policy","code":true},{"text":", because the state is a value you stored, not a position in a transcript."}],[{"text":"Cost.","b":true},{"text":" Each state gets a short prompt and only the context it needs. Three short calls beat one twenty-turn transcript, and the arithmetic from the context-window lesson makes that difference large."}],[{"text":"Small prompts.","b":true},{"text":" Each state’s instruction is a paragraph about one decision, which is far easier to write, test and ablate than a page trying to govern everything at once."}]]},{"t":"h2","id":"keep-loops-where-they-belong","runs":[{"text":"Keep loops where they belong"}]},{"t":"p","runs":[{"text":"Inside a state, when the work is exploratory (searching until something is found, retrying a flaky lookup, reading files until a question is answered) use a loop with its own budget. The graph is the outer structure; loops live inside individual nodes. Most good agent systems are shaped like this rather than being purely one or the other."}]},{"t":"h2","id":"what-to-build-it-with","runs":[{"text":"What to build it with"}]},{"t":"p","runs":[{"text":"A dictionary of handler functions and "},{"text":"while state != \"done\"","code":true},{"text":" is a complete implementation and stays debuggable at 3am:"}]},{"t":"code","lang":"python","label":"Python","text":"state, ctx = \"verify_order\", {\"order_id\": order_id}\nwhile state != \"done\":\n    state, ctx = HANDLERS[state](ctx)\n    log(run_id, state, ctx)"},{"t":"p","runs":[{"text":"LangGraph","b":true},{"text":" gives you this with persistence and streaming attached. "},{"text":"Temporal","b":true},{"text":" gives durable execution, meaning a workflow survives the machine dying mid-step, which matters when a state takes ten minutes and calls a payment API. Both are real tools with real costs: a framework you cannot read is a debugging problem you have deferred, not avoided. Start with the dictionary; move when you can name what you need."}]},{"t":"h2","id":"the-limitation","runs":[{"text":"The limitation"}],"role":"limit"},{"t":"p","runs":[{"text":"A state machine encodes a process you already understand. If nobody can draw the process, you cannot draw the graph, and forcing one prematurely produces a rigid system that handles the twelve cases somebody imagined and breaks on the thirteenth."}]},{"t":"p","runs":[{"text":"So the sequence that works is: prototype with a free loop, run it on real inputs, then "},{"text":"read fifty traces and extract the graph the traces already show you","b":true},{"text":". The states will be obvious by then, and so will the two transitions you would never have predicted. Building the loop first is not wasted work; it is how the map gets drawn."}]}],"check":{"question":[{"text":"A team converts a free-running refund agent into five explicit states. Beyond being easier to test, why does this reduce the damage a prompt injection in a customer email can do?"}],"options":[{"text":[{"text":"Each state exposes only the tools it needs, so the refund tool is absent, not merely forbidden, while the agent is reading customer text."}],"correct":true,"tempting":null},{"text":[{"text":"With fewer choices at each step, the model is more accurate and less likely to be misled."}],"correct":false,"tempting":[{"text":"Narrower choice does tend to improve accuracy, but accuracy is not containment: a more accurate model still holds the dangerous tool."}]},{"text":[{"text":"The transcript in each state is shorter, so there is less room for injected text to influence the outcome."}],"correct":false,"tempting":[{"text":"Shorter context reduces dilution effects, but a single injected sentence in a short prompt is if anything more influential, not less."}]},{"text":[{"text":"The run passes through fewer total steps, so there are fewer opportunities for an error to compound."}],"correct":false,"tempting":[{"text":"Confuses compounding error with an adversarial input; one hostile message needs only one opportunity to matter."}]}],"order":[1,0,3,2],"why":null},"prev":{"slug":"budgets-and-stopping-rules","title":"Budgets belong in the harness, not the prompt","minutes":7},"next":{"slug":"retries-and-idempotency","title":"Retries, timeouts and doing it twice","minutes":8}}}