What you'll learn: how to decide with four yes-or-no questions whether a task needs a single call, a bounded workflow or an agent loop, why the catalog problem gets a workflow, and how to write that workflow in plain Python with a test that proves what it does.
At a glance
Figure 1. Four questions pick the control flow
How to read this: start at the top, follow yes or no, and stop at the first rectangle you reach.
- A workflow is code that fixes the order of the steps. An agent loop lets the model choose the next step.
- The catalog task reaches "Workflow in code" at the second question.
- Write the plain-code version first; a framework is a later decision.
- The model still does the language work inside a workflow.
Scenario
Composite scenario, not a real company. Lumen Industrial Supply's sales engineers receive requests such as "monitor vibration on 40 pumps in a humid plant, budget around 30k, must work with our existing OPC UA setup." A manager asks for "an agent for product discovery." The tools exist: search the catalog, read one product record, check whether a product speaks a protocol. Every tool only reads. All numbers are illustrative.
What you will be able to do
- Answer the four questions for a task and name the control flow they select.
- Write a fixed workflow for the catalog task in plain Python and test it against the capstone's planner.
- Count model calls and estimate relative cost for a workflow and for a model-planned loop.
- Explain in three sentences why you would not start with a framework.
Prerequisites
- Tool calling with one read-only tool and Evaluating retrieval and answers.
- Agent-loop theory is in Agents, tools and MCP; this page adds the hands-on decision.
Core ideas
Three shapes of control
| Shape | Who chooses the next step | How you test it | How it fails |
|---|---|---|---|
| Single call | Nobody: one request, one answer | One input and one expected output | Wrong or missing facts |
| Workflow | Your code | Ordinary unit tests on each step | A step is wrong or missing |
| Agent loop | The model, every turn | Many runs, graded by a set of cases | Loops, wrong tool, wrong arguments |
Anthropic's Building effective agents uses the same split: workflows are "systems where LLMs and tools are orchestrated through predefined code paths", agents are systems where LLMs "dynamically direct their own processes and tool usage". Its advice is "finding the simplest solution possible, and only increasing complexity when needed."
Predict first. For the catalog task, a planner model is asked "what is the next step?" on every turn. If you ran it ten times on the same request, how many different tool sequences would you expect to see in production?
Answer
Probably more than one, and that is the cost: a real model can reorder, repeat or skip steps. The capstone's MockProvider is a rule-based script, so it always produces one sequence, which makes it a workflow with extra steps.
The catalog task is a workflow
The sequence never changes: search, read the top candidates, check each required protocol, read a gateway if the protocol is missing. Only the number of candidates varies, and a loop handles that.
Figure 2. The catalog workflow as states
How to read this: every arrow is a line of code you can unit test; the model chooses none of them.
| Decision | Made by | Why |
|---|---|---|
| Turn the request into requirements | Model | Free text in, structured fields out |
| Which tool comes next | Code | The order is known |
| How many candidates to read | Code | A constant, easy to change |
| Write the recommendation text | Model | Language out, checked by code afterwards |
Plain code before a framework
The same article says frameworks "often create extra layers of abstraction that can obscure the underlying prompts" and suggests developers "start by using LLM APIs directly". A reader who has written the loop once can read any framework's loop.
At the customer: a manager asks for "an agent". Ask what decision the model would make that code cannot. If the answer is "none", propose a workflow and revisit when a task needs a variable plan.
Worked example
Count the model calls for one request in the capstone with the mock provider. Token counts come from the capstone's rough estimator, and the prices are placeholder prices from app/config.py (1 and 5 euros per million input and output tokens). Fill in your provider's published prices before quoting a number.
Assumptions: one request, the catalog request above, public caller.
| Call type | Calls | Input tokens | Output tokens |
|---|---|---|---|
| Extract requirements | 1 | 77 | 32 |
| Plan the next step | 7 | 2,162 | 130 |
| Compose the answer | 1 | 553 | 134 |
| Total | 9 | 2,792 | 296 |
| Version | Arithmetic (millionths of a euro) | Cost at placeholder prices |
|---|---|---|
| Model-planned loop | 2,792 x 1 + 296 x 5 = 4,272 | 0.004272 euro |
| Order fixed in code | 630 x 1 + 166 x 5 = 1,460 | 0.00146 euro |
With the order fixed in code, the seven planner calls disappear. The model-planned version costs 4,272 / 1,460, about 2.9 times more, for the same six tool calls. Planner calls also grow, from 132 to 462 input tokens, because each resends the earlier observations.
Build it
The decision tree from Figure 1 is a small function; answer its four traits from the task, not from taste.
Verified: capstone/fde-product-discovery-copilot/lessons/M4-L1/m4_l1_workflow.py
def choose_control_flow(traits: TaskTraits) -> ControlFlow:
"""The decision tree of Figure 1, top to bottom. The first question that fires decides."""
if traits.one_call_is_enough:
return "single_call"
if traits.steps_known_in_advance:
return "workflow"
if not traits.wrong_step_can_be_undone_or_gated:
return "workflow_with_human"
if not traits.success_measurable_and_capped:
return "build_evaluation_first"
return "agent_loop"
The workflow is a short function over the registry from the next lesson. registry.call never raises, so a failed step is recorded and the workflow carries on.
Verified: capstone/fde-product-discovery-copilot/lessons/M4-L1/m4_l1_workflow.py
def run_fixed_workflow(req: Requirements, registry: ToolRegistry, ctx: ToolContext) -> list[Step]:
"""Search, read the top candidates, check each protocol, read the first bridging gateway."""
steps: list[Step] = []
def call(tool: str, args: dict[str, Any]) -> dict[str, Any] | None:
outcome = registry.call(tool, args, ctx)
steps.append(Step(tool, args, outcome.ok))
return outcome.data if outcome.ok else None
found = call("search_catalog", search_args(req))
candidates = [p["sku"] for p in found["products"][:3]] if found else []
for sku in candidates:
call("get_product_spec", {"sku": sku})
bridges: list[str] = []
for sku in candidates:
for protocol in req.protocols:
check = call("check_compatibility", {"sku": sku, "protocol": protocol})
if check and not check["supported"]:
bridges += [g for g in check["via"][:1] if g not in bridges]
for sku in bridges[:2]:
call("get_product_spec", {"sku": sku})
return steps
The test compares this workflow with the capstone's model-planned runner. The mock follows a fixed script, so the sequences are equal; a real model might not match.
Verified: capstone/fde-product-discovery-copilot/lessons/M4-L1/test_m4_l1_workflow.py
def test_plain_code_makes_the_same_calls_as_the_scripted_planner(ctx: ToolContext) -> None:
req = extract_requirements(REQUEST)
fixed = run_fixed_workflow(req, default_registry(), ctx)
planned = WorkflowRunner(MockProvider(), default_registry(), ctx).run(req)
# The mock "model" follows a fixed script, so the two sequences match. A real model may differ.
assert [(s.tool, s.args) for s in fixed] == [(o["tool"], o["args"]) for o in planned.observations]
Running the workflow on the catalog request printed this (real output):
1 search_catalog {'query': 'vibration pump humid OPC UA', 'limit': 5, 'category': 'sensors', 'max_unit_price_eur': 750.0, 'min_ip': 65} ok
2 get_product_spec {'sku': 'LUM-VS-100'} ok
3 get_product_spec {'sku': 'LUM-VS-200'} ok
4 check_compatibility {'sku': 'LUM-VS-100', 'protocol': 'OPC UA'} ok
5 check_compatibility {'sku': 'LUM-VS-200', 'protocol': 'OPC UA'} ok
6 get_product_spec {'sku': 'LUM-GW-200'} ok
The price cap of 750 is the 30,000 budget divided by 40 pumps, which assumes one sensor per pump. Tell the customer that assumption.
Exercise
Decision exercise. A second Lumen request arrives: "A customer emailed about a delayed order. Find the order, read the carrier status, and draft a reply. If the customer asks for a refund, a person decides." Options: (A) one model call with the email and the order pasted in, (B) a workflow, (C) an agent loop that can also issue refunds. Choose one and justify it with the four questions.
Model answer
Choose B. One call is not enough because the order and carrier data live in other systems. The steps are known: find the order, read the status, draft the reply. A refund is a decision for a person. Option C fails the third question: the loop cannot undo a wrong refund.
Explain it back. Explain to a customer's engineering manager, in three sentences, why the product discovery assistant is a workflow and not an agent. A good explanation contains:
- the property of the task that decides it (a fixed order of steps);
- what the model still does (reads the request, writes the answer);
- what would make you revisit the choice (steps that cannot be listed in advance).
Task: add a TaskTraits for the delayed-order task to a copy of the test file, assert the result with choose_control_flow, and write one sentence naming the trait you were least sure about and what evidence would change your answer. Done when a colleague who has not read this lesson can read your test and see why the answer is a workflow.
Hint 1: which trait is hardest?
Look at steps_known_in_advance: can you list the steps before the email arrives, including when the order is not found?
Hint 2: what does "gated" mean here?
wrong_step_can_be_undone_or_gated is true when a person or a rule stands between a mistake and its effect. Reading an order is undoable; issuing a refund is not, unless approval stands in front of it.
Common mistakes
- Choosing "agent" because the word is in the project title, before answering the four questions.
- Answering "steps known in advance" with "mostly". If the order can change, write down what changes it.
- Letting the model decide the order and testing only the final text, so a wrong tool sequence goes unseen.
- Reading the mock planner's matching output as proof that a real model will plan the same way.
Knowledge check
1. A task needs facts from three internal systems in a fixed order. Which control flow do the four questions select?
A workflow in code. The facts are outside the model, so one call is not enough, and the steps are known in advance, so the second question ends the tree.
2. Why did the model-planned version cost about 2.9 times more in the worked example?
It made seven extra planner calls, each resending earlier observations, so inputs grew from 132 to 462 estimated tokens. The tool calls are identical.
3. The test shows that plain code and the mock planner make the same six calls. What does that not prove?
That a real model would plan the same way. The mock is a script, so equal sequences show only that the script is a fixed workflow. A real model can reorder, repeat or skip steps.
4. When would you move this task from a workflow to an agent loop?
When real tasks appear whose steps cannot be listed in advance (different tools depending on what earlier tools returned) and you can measure success and cap steps and time. Until then the extra cost buys nothing.
5. What does the price cap of 750 euros in the first search call assume?
One sensor per pump: the 30,000 budget divided by 40 pumps. If the customer needs several per pump, or gateways in the budget, the cap is wrong, so state the assumption.
You can now
- Answer four questions about a task and say which control flow they select.
- Write a fixed tool workflow in plain Python and compare it with a planner.
- Count planner calls and estimate relative cost with placeholder prices.
- Explain why a framework is a later decision.
Further reading
- Building effective agents, Anthropic Engineering, 2024-12-19: the workflow and agent definitions used here.
- Handle tool calls, Claude API documentation: receiving a proposed tool call and returning the result.
- The AI Architect lesson Agents, tools and MCP for loop budgets.
Last verified: 2026-11-02