AI Agents vs Workflows: When You Actually Need Autonomy
Somebody on your team is about to build an "agent" for a task that could be solved with four function calls and an if-statement. This happens constantly. The word agent has become a status symbol in engineering meetings — say it and the project sounds more advanced than it is. Meanwhile the actual system, once you look at the code, is a fixed sequence: fetch data, summarize it, format it, send it. That is not agentic. That is a workflow, and workflows are usually the right answer.
This confusion costs real money and real reliability. Teams add a planning loop, a tool-selection step, and open-ended retries to a process that had exactly one correct path all along — and then wonder why it's slower, more expensive, and fails in new and exciting ways it never used to. Other teams do the opposite: they hardcode a rigid pipeline for a problem that is inherently unpredictable, and it snaps the first time a user does something the flowchart didn't anticipate. Both mistakes come from the same root cause — not having a clear test for which one you're building.
This article gives you that test. We'll define the core distinction precisely, walk through the tradeoffs of full autonomy, show a concrete code contrast, cover hybrid patterns that get you 90% of an agent's flexibility with 10% of its chaos, and end with a rule of thumb you can apply in the next design review.
The core distinction: known path vs unknown path
Strip away the marketing and there's one question that separates a workflow from an agent: does the system know its own next step in advance, or does it decide at runtime?
A workflow is a fixed, deterministic sequence of steps — some of which may call an LLM — wired together by code you wrote. The control flow lives in your codebase, not in the model's output. Step 2 always follows step 1. If step 3 is "call the refund API," that call happens because your code says so, not because the model chose to invoke it. The LLM might be doing something intelligent inside a step — extracting structured data, writing a summary, classifying a ticket — but it is not choosing *which step happens next*. You already knew that when you drew the diagram.
An agent is a system where the model itself decides the next action, chooses which tool to call, evaluates the result, and decides again — in a loop — until it determines the task is done. The path is not fixed. Ask an agent to "fix the failing test," and it might read the file, run the test suite, grep for a related function, edit two files, run the tests again, and stop when they pass. You did not write that sequence. The model produced it, step by step, in response to what it observed.
This is the entire distinction. Not "does it use an LLM" — both use LLMs. Not "is it smart" — a workflow can call a very capable model at every step. The distinction is where the control flow lives. If you can draw the flowchart before running the system and that flowchart is always correct, it's a workflow. If the flowchart can only be drawn *after* the run, by tracing what actually happened, it's an agent.
This matters because the two have completely different failure modes, cost profiles, and debugging stories, and conflating them is where most production pain comes from.
The 20-line test
Here's a fast, practical filter before you write any agent code: can you write the control flow in about 20 lines of plain code, without an LLM deciding what happens next?
Try it. Take the task and sketch the steps as a numbered list, the way you'd explain it to a junior engineer:
- Pull the ticket text.
- Classify it into one of five categories with an LLM call.
- Look up the category's SLA.
- Draft a response with an LLM call, using the category and SLA as context.
- If the category is "billing," attach the account's invoice history.
- Send the draft to a human for approval.
That's six lines and two LLM calls. Nothing here required the model to choose its own next step — you already knew step 5 depends on step 2's output, and you wrote that dependency yourself. This is a workflow, full stop. Building an agent for this would mean asking the model, at every step, "what should I do next," when the honest answer is always the same regurgitated plan you already had in your head.
Now try it on something else: "Investigate why the deployment failed and fix it if possible." Sketch the steps.
- ...check the logs?
- ...but which logs, depends on what the error looks like.
- ...maybe rerun the build, maybe check a config diff, maybe it's a dependency issue, maybe it's a flaky test.
You can't get past step 1 without already knowing the answer, which is the entire point — you don't know it. The number of branches multiplies with each new failure type you imagine, and no fixed list of steps covers the space. This is the signature of a genuine agent problem: the branching factor is too high and too unpredictable to enumerate in advance.
The 20-line test isn't about literally counting lines. It's about honesty. If you find yourself writing a giant nested tree of if/elif branches trying to anticipate every path a workflow *might* need, that complexity is telling you something — the task has unknown-in-advance structure, and you're fighting the tool by trying to force determinism onto it. That's a real signal to consider an agent. But if you're adding agent machinery to a task that passed the 20-line test cleanly, you're adding autonomy the task never asked for.
What full autonomy actually costs you
Autonomy is not free, and it's worth being blunt about the bill, because "just make it an agent" is a decision with three concrete costs.
Cost. Every autonomous step is another round trip to a model, and agent loops routinely make multiple calls to figure out what a workflow would have done in one. A workflow that calls an LLM twice, deterministically, costs you two calls, always. An agentic loop solving the same nominal task might take two calls on a good run and eight on a bad one, because it explores a dead end, backtracks, or re-reads a tool result it misinterpreted the first time. You're not paying for the answer — you're paying for the model's search process to find the answer, and that search is not bounded unless you bound it yourself.
Latency. Each decision point in an agent loop is a full model call, and those are sequential by nature — the model can't decide step 4 before it sees the result of step 3. A workflow can parallelize independent LLM calls trivially, because you know upfront they don't depend on each other. An agent, by definition, might not know that either, so it often serializes work that didn't need to be serial, just because it's discovering the dependency graph live instead of having it handed over.
Predictability. This is the big one, and it's the one people underestimate. A workflow fails in ways you can enumerate: the API times out, the classification is wrong, the summary is too long. Each of those has a known fix. An agent fails in ways you cannot fully enumerate in advance: it picks a plausible-looking but wrong tool, it decides a task is finished when it isn't, it loops on a step that isn't actually making progress, it "fixes" something adjacent to the actual bug. Debugging a workflow means finding the step that broke. Debugging an agent means reconstructing *why the model believed* a certain action was the right one at that moment — which is a fundamentally harder kind of debugging, closer to reading someone else's reasoning after the fact than tracing a stack trace.
None of this means agents are bad. It means autonomy is a resource you spend, not a feature you get for free by adding a loop. Spend it on the parts of the problem that actually have unknown structure, and not on the parts you already understand perfectly well.
A concrete contrast: workflow function vs agentic loop
Here's the shape difference in code. First, a deterministic workflow — support ticket triage. Every step is fixed; the LLM is used for two narrow judgment calls, but the sequence around them never changes.
def handle_support_ticket(ticket: dict) -> dict:
# Step 1: classify — LLM call, but the NEXT step is fixed regardless of output
category = llm_classify(
text=ticket["body"],
labels=["billing", "bug", "feature_request", "account", "other"],
)
# Step 2: deterministic lookup, no LLM involved
sla_hours = SLA_TABLE[category]
# Step 3: conditionally attach context — the branch exists, but it's YOUR branch
context = {}
if category == "billing":
context["invoices"] = fetch_invoice_history(ticket["account_id"])
# Step 4: draft a reply — LLM call, output is text, not a decision about what runs next
draft = llm_draft_reply(
ticket_body=ticket["body"],
category=category,
sla_hours=sla_hours,
context=context,
)
# Step 5: fixed next step — always goes to a human queue
return enqueue_for_approval(draft, category=category, sla_hours=sla_hours)Every line here was decided by you, at design time. The LLM contributes judgment (which category, what words to use in the draft) but never contributes *control flow*. Run this a thousand times and the shape of execution is identical every time — only the classification and draft text vary.
Now contrast that with the shape of an agentic loop for something like "investigate and fix this failing build":
def agent_loop(goal: str, tools: list, max_steps: int = 15) -> str:
messages = [{"role": "user", "content": goal}]
for step in range(max_steps):
response = llm_call(messages, tools=tools) # model decides next action
if response.is_final_answer:
return response.content
# The MODEL chose which tool to call and with what arguments —
# your code didn't decide this, it just executes what was chosen
tool_result = execute_tool(response.tool_call.name, response.tool_call.args)
messages.append(response.to_message())
messages.append({"role": "tool", "content": tool_result})
return "Stopped: max steps reached without resolution"Notice what changed. There is no fixed sequence of tool calls in this function — there can't be, because response.tool_call.name is decided by the model on each iteration based on what it saw in the previous result. The only things your code controls are the tool list, the stopping condition, and the step budget. Everything about *which* tools get called, in *what* order, and *when to stop* is delegated. That delegation is the entire value proposition of an agent — and also the entire source of its unpredictability. You're trading a flowchart for a search process, on purpose, because the task's branching factor made the flowchart impossible to draw honestly.
Hybrid patterns: the workflow with one agentic step
Most production systems that are described as "agents" are actually this: a deterministic workflow with exactly one bounded agentic step dropped in where genuine unpredictability lives, and fixed steps everywhere else.
Take a document-processing pipeline. Ingest a PDF, extract text, and file it correctly. The filing step is genuinely unpredictable — you don't know in advance which of forty folders a given document belongs in, and the reasoning might require re-reading the document, checking a related file, or asking a clarifying sub-question. Everything around it is not:
def process_document(file_path: str) -> str:
# Fixed step
raw_text = extract_text(file_path)
# Fixed step
metadata = parse_metadata(raw_text)
# ONE bounded agentic step — the only part with real uncertainty
# Constrained: limited tool set, small step budget, must return one of N folder paths
destination_folder = agent_loop(
goal=f"Determine the correct filing folder for this document: {metadata}",
tools=[search_existing_folders, read_related_document, list_folder_structure],
max_steps=5,
)
# Fixed step
move_file(file_path, destination_folder)
# Fixed step
return log_filing_decision(file_path, destination_folder)This is the pattern to reach for by default, not the fully autonomous loop. It gives you the flexibility exactly where the task needs it — the filing decision — while keeping ingestion, logging, and the final move under your direct control, where a stray decision from the model would be an actual bug rather than a reasonable judgment call. It's also dramatically easier to debug: if a document ends up in the wrong folder, you know precisely where to look, because everything except one bounded step is deterministic.
Other common hybrid shapes worth knowing:
- Router pattern: a small classification step (workflow) decides which of several *separate* workflows to run — not which tool to call next, just which pre-built pipeline applies. This looks agentic but is 100% predictable per branch.
- Workflow with a retry-and-repair loop: a fixed sequence where one step, if it fails validation, gets handed a small bounded agentic retry ("here's what came back, here's why it's invalid, try again with these tools") capped at 2-3 attempts before falling back to a human.
- Plan-then-execute: an LLM call produces a plan up front (a list of steps), and then those steps run as a fixed workflow — this is agent-*flavored* but actually collapses into a workflow the moment the plan is generated, since execution doesn't re-decide anything.
Each of these gives you a slice of adaptability without paying the full cost of an open-ended loop everywhere.
Failure modes of over-engineering a workflow into an agent
These are the failures that show up in production when a team reaches for autonomy the task didn't need, and they're worth naming specifically because they don't look like "the agent is broken" — they look like mysterious cost spikes and inconsistent quality.
Silent scope creep in tool selection. A workflow that always calls the same three well-tested functions in the same order gets replaced by a loop that "can call any of twelve tools." The model now occasionally reaches for a tool that technically fits the description but produces a subtly wrong result for this specific case — something the original fixed sequence would never have done, because you'd already decided the right tool for that slot.
Non-reproducible bugs. A user reports a bad output. You rerun the exact same input. You get a different sequence of tool calls and a different, also-plausible-looking wrong answer. In a workflow, the same input reliably reproduces the same bug, so you can fix it once. In an over-agentified system, "reproduce the bug" itself becomes a research project.
Runaway loops disguised as thoroughness. Without a tight step budget and a crisp stopping condition, an agent can keep "investigating further" well past the point of diminishing returns — rereading the same file, re-querying the same API with slightly different phrasing — burning steps and money on a task that a three-step workflow would have closed out immediately.
Approval and audit friction. Fixed workflows are easy to get signed off by compliance or ops, because you can show the exact sequence of what happens with sensitive data. The moment that sequence becomes "whatever the model decides at runtime," every stakeholder conversation gets harder, because you're now defending a distribution of possible behaviors instead of one behavior.
Harder onboarding for the next engineer. A new team member can read a workflow function top to bottom and understand the system in five minutes. An agent's actual behavior lives in the interaction between the prompt, the tool descriptions, and the model's judgment — nobody can read that and know what will happen without running it. Every unnecessary agentic step is a permanent tax on how legible your system is to the next person who has to touch it.
None of these are hypothetical edge cases — they're the default outcome of adding a decision loop to a problem that had exactly one right sequence of steps all along.
When autonomy earns its cost
To be fair to the other side: there are tasks where a workflow is actively the wrong shape, and forcing determinism onto them produces a brittle system that breaks on the first case the flowchart-author didn't imagine.
Signs you're in genuine agent territory: the number of plausible next steps depends on information you only get *during* execution (an error message, a search result, a user's follow-up), the task has an open-ended goal rather than a fixed output shape ("get this test suite passing" rather than "extract these five fields"), and the cost of occasionally taking an inefficient path is much lower than the cost of the task failing outright because your fixed pipeline didn't anticipate a case.
Coding assistants that read a codebase and fix a bug are a legitimate agent use case — there's no fixed list of files to check, no fixed number of edits, no way to know in advance whether the fix touches one function or four. Research tasks that need to follow a citation trail wherever it leads are legitimate. Customer support escalations where the resolution genuinely depends on a multi-turn back-and-forth with an external system you don't control are legitimate. In each case, the branching factor is real, not decorative — you couldn't shrink it to twenty lines of code no matter how hard you tried, because the task's structure is discovered, not known.
The test is symmetric: just as you shouldn't force autonomy onto a fixed sequence, you shouldn't force a fixed sequence onto a genuinely unpredictable task, either. Both directions of misfit cost you — one in wasted spend and fragility, the other in a brittle pipeline that breaks the first time reality doesn't match the flowchart.
The rule of thumb
Default to a workflow. Reach for an agent only when the 20-line test fails honestly — when you try to enumerate the steps and the branching factor explodes because the actual path depends on information you don't have until runtime.
When you do need autonomy, don't hand the whole system over to a loop. Isolate the genuinely unpredictable part into one bounded agentic step — capped step budget, restricted tool list, clear stopping condition — and keep every step around it, ingestion, logging, formatting, delivery, as plain deterministic code. This gives you almost all the adaptability a full agent would offer, at a fraction of the cost, latency, and debugging burden, and it keeps the system legible to the next engineer who opens the file.
Autonomy is a tool for handling genuine uncertainty, not a stamp of technical sophistication. The best agentic systems in production today are mostly workflows, with autonomy spent carefully on the one or two steps that actually need it — and the discipline to say "this is just a workflow" out loud, even when "agent" would sound more impressive in the standup.
If you want to go deeper on where the loop itself comes from, how to design tool interfaces the model can actually reason about, and how to put real guardrails around an agentic step instead of just capping the step count and hoping — that's exactly what we built "Introduction to AI Agents" (free course) to walk through, step by step, with real code.
BootcampA 30-day guided bootcamp: build, harden and ship a production autonomous agent from scratch.
AI AgentsUnderstand how AI agents really work: the loop, the tools, the memory, and why most agent projects fail.
Related reading