Workflows vs Agents: When an LLM App Needs Autonomy
Learn the difference between LLM workflows and agents, when each is the right choice, and how to build the same feature both ways in TypeScript to compare them.
On this page· Two kinds of systems
Why this matters
"Agent" is the most overused word in AI engineering. Teams build autonomous agents for tasks a three-step script would do better, then fight unpredictable costs and bugs they cannot reproduce. Knowing when a task needs autonomy, and when it needs plain code calling a model, is the most important design decision in an LLM feature. It decides cost, latency, reliability, and how easy the system is to test.
Two kinds of systems#
Both workflows and agents combine LLM calls with tools. The difference is who decides what happens next:
- In a workflow, your code defines the path. The model fills in steps (classify this, draft that), but the sequence is fixed or chosen by ordinary
ifstatements. - In an agent, the model decides the path. It gets a goal and tools, and chooses which tool to call next, based on what it has learned so far, until it judges the goal met.
Prompt chaining
Tap a step for details.
Show text descriptionHide text description
Input flows through LLM call 1, a code gate that can exit early, LLM call 2, and LLM call 3 to the output.
Use it when: The task splits cleanly into ordered subtasks and you want each call to be simpler and more accurate.
Example: Write an outline, check it covers required points, draft the article, then polish the tone.
Autonomous agent
Tap a step for details.
Show text descriptionHide text description
A human gives the LLM agent a task; the agent repeatedly takes actions in its environment and reads the feedback, checks in with the human when needed, and stops when done.
Use it when: The task is open-ended, the number of steps cannot be predicted, and you can afford the cost and verify the outcome.
Example: Fix a failing test: read the code, run the tests, edit files, re-run, and stop when the suite passes.
Neither is better. A workflow is predictable: the same input takes the same path, so it is cheap to test and easy to debug. An agent is flexible: it can handle tasks where the steps cannot be known in advance, such as debugging an unfamiliar codebase or researching an open question.
Climb only as far as you need#
Hover or tap a component to see what it does.
Show text descriptionHide text description
Each step to the right gives the model more control over what happens next. Climb only as far as the task needs.
- Single call: Classification, extraction, summarization, drafting. Cheapest and most predictable. Start here.
- Workflow: Several LLM calls and tools in a path your code defines: chains, routing, parallel steps. Predictable, testable, easy to debug.
- Agent: The model chooses tools in a loop until the goal is met. Flexible for open-ended tasks; costs and failure modes grow with autonomy.
- Multi-agent: An orchestrator delegates to subagents with their own context. Powerful for broad research; several times the tokens, and hardest to debug.
| Single call | Workflow | Agent | |
|---|---|---|---|
| Who picks the steps | Nobody (one step) | Your code | The model |
| Latency | Seconds | Seconds, predictable | Seconds to minutes, variable |
| Cost per task | Lowest | Low, predictable | Higher, variable |
| Testing | Unit-test the prompt | Test each step and the path | Evaluate outcomes across many runs |
| Fits | Extraction, classification, drafting | Known multi-step processes | Open-ended tasks with unknown steps |
A good rule: start with the simplest level that could work, measure it, and move up only when you can point to the failures that more autonomy would fix.
The same feature, built both ways#
The task: a customer emails support, and the system drafts a reply. For billing questions, it should look up the customer's payments first.
As a workflow#
The steps are known in advance: classify, fetch data if needed, draft. Code decides the path; the model does the language work.
// Stand-ins for your services.
export async function findCustomer(email: string) {
return email === "ana@example.com" ? { id: "cus_1", name: "Ana", plan: "pro" } : null;
}
export async function listPayments(customerId: string) {
return customerId === "cus_1"
? [
{ id: "py_1", amount: 49, status: "succeeded", date: "2026-10-01" },
{ id: "py_2", amount: 49, status: "pending", date: "2026-10-01" },
]
: [];
}import Anthropic from "@anthropic-ai/sdk";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
import { z } from "zod";
import { findCustomer, listPayments } from "./lib/data.ts";
const client = new Anthropic();
const Classification = z.object({ category: z.enum(["billing", "technical", "other"]) });
export async function draftReplyWorkflow(from: string, email: string): Promise<string> {
// Step 1: classify (a narrow task, so a fast model at low effort).
const classified = await client.messages.parse({
model: "claude-sonnet-5-5",
max_tokens: 1024,
output_config: { effort: "low", format: zodOutputFormat(Classification) },
messages: [{ role: "user", content: `Classify this support email:\n<email>\n${email}\n</email>` }],
});
const category = classified.parsed_output?.category ?? "other";
// Step 2: code, not the model, decides whether to fetch data.
let context = "";
if (category === "billing") {
const customer = await findCustomer(from);
if (customer) context = JSON.stringify({ customer, payments: await listPayments(customer.id) });
}
// Step 3: draft the reply with exactly the context it needs.
const draft = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 2048,
system: "You draft friendly, concise replies for a support agent to review. Use only the account data provided.",
messages: [{ role: "user", content: `<account>${context || "none"}</account>\n<email>\n${email}\n</email>` }],
});
return draft.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join("");
}
console.log(await draftReplyWorkflow("ana@example.com", "Hi, I see two charges of $49 this month. Why?"));As an agent#
The agent gets the goal and the tools, and decides on its own whether and in what order to look things up:
import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";
import { findCustomer, listPayments } from "./lib/data.ts";
const client = new Anthropic();
const tools = [
betaZodTool({
name: "find_customer",
description: "Find a customer by email address. Returns id, name, and plan, or null.",
inputSchema: z.object({ email: z.string() }),
run: async ({ email }) => JSON.stringify(await findCustomer(email)),
}),
betaZodTool({
name: "list_payments",
description: "List a customer's recent payments with amount, status, and date.",
inputSchema: z.object({ customer_id: z.string() }),
run: async ({ customer_id }) => JSON.stringify(await listPayments(customer_id)),
}),
];
export async function draftReplyAgent(from: string, email: string): Promise<string> {
const final = await client.beta.messages.toolRunner({
model: "claude-opus-5-5",
max_tokens: 4096,
max_iterations: 6,
system: "You draft friendly, concise replies for a support agent to review. Look up account data when it helps.",
tools,
messages: [{ role: "user", content: `Email from ${from}:\n<email>\n${email}\n</email>` }],
});
return final.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join("");
}
console.log(await draftReplyAgent("ana@example.com", "Hi, I see two charges of $49 this month. Why?"));For this task, the workflow wins: the steps are known, it makes exactly three calls, and each step can be tested on its own. The agent version starts paying off when the space of possible steps grows, for example with twenty tools across billing, shipping, and accounts, where writing every path by hand becomes the harder problem.
Signs you need an agent#
- The steps depend on what earlier steps discover, and you cannot enumerate the paths.
- There are many tools, and which ones a task needs varies widely.
- Success can be checked (tests pass, the answer cites sources), so the agent can tell when it is done.
- The task is valuable enough to justify variable cost and latency.
Signs you need a workflow#
- You can draw the flowchart.
- Latency or cost must be predictable.
- Regulated or high-risk steps must always happen in a fixed order.
- You need to explain exactly why the system did something.
Many production systems are workflows with an agent inside one step: a fixed pipeline that hands one open-ended step, such as investigating an unusual case, to a bounded agent.
Common mistakes#
- Starting with a multi-agent framework. Build the single call first. Most features never need more.
- Agents for fixed processes. If you can write the steps down, write them in code.
- No limits on agents. Every agent needs caps on turns, tokens, and time.
- Judging an agent on one run. Agents vary between runs; evaluate on many tasks.
Try it
Compare both versions on the same inputs
- Write six support emails: three billing questions, two technical, one unclear.
- Run both
workflow.tsandagent.tson each, recording the number of model calls, total tokens (fromusage), and latency. - Rate each draft from 1 to 5 and decide which design you would ship. Write down what would have to change for the other to win.
Check your understanding
Key takeaways
- In a workflow, code decides the steps; in an agent, the model does.
- Start with a single call, then a workflow, and move to an agent only when you can name the failures autonomy would fix.
- Workflows are predictable and easy to test; agents handle open-ended tasks at variable cost.
- Many good systems are workflows with one bounded agent step.
Next steps#
Finished this article? Track it to see your progress on the roadmap.
Related articles
- Agent Design Patterns in TypeScript: Chaining to Evaluator-OptimizerSix proven patterns for LLM systems, with TypeScript code: prompt chaining, routing, parallelization, orchestrator-workers, evaluator-optimizer, and autonomous agents.
- Build an AI Agent Loop from Scratch in TypeScriptWrite a production-ready agent loop in TypeScript: Zod-typed tools, turn, token, and time limits, parallel tool calls, error handling, events for your UI, and unit tests.
- Tool Use (Function Calling) with Claude in TypeScriptLet the model call your functions: define tools, run the tool-use loop by hand, handle errors and parallel calls, then use the SDK tool runner with Zod-typed tools.