Skip to content
AI Full-Stack Wiki

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.

Difficulty: Intermediate14 min readUpdated
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 if statements.
  • 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

Prompt chaining patternInput flows through LLM call 1, a code gate that can exit early, LLM call 2, and LLM call 3 to the output.passfailInputLLM call 1outlineGatecode checkExitfails checkLLM call 2draftLLM call 3polishOutput

Tap a step for details.

Prompt chaining. A fixed sequence of LLM calls where each step works on the previous step's output, with programmatic checks in between.
Show 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

Autonomous agent patternA 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.taskcheck-insactionfeedbackHumanLLM agentdecides next stepEnvironmenttools, APIs, filesStop

Tap a step for details.

Autonomous agent. An LLM decides its own next step in a loop: it calls tools, observes the results, and continues until the goal is met or a limit is reached.
Show 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#

The autonomy ladderFour levels of LLM systems in order of increasing autonomy and cost: a single call, a workflow of fixed steps, an agent that chooses its own steps, and a multi-agent system.needs stepssteps unknowntoo big for oneSingle callone prompt, one answerWorkflowcode decides the stepsAgentmodel decides the stepsMulti-agentagents coordinate

Hover or tap a component to see what it does.

The autonomy ladder: move up from a single LLM call to a workflow, an agent, or a multi-agent system only when the task requires it, because cost, latency, and unpredictability grow at each step.
Show 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 callWorkflowAgent
Who picks the stepsNobody (one step)Your codeThe model
LatencySecondsSeconds, predictableSeconds to minutes, variable
Cost per taskLowestLow, predictableHigher, variable
TestingUnit-test the promptTest each step and the pathEvaluate outcomes across many runs
FitsExtraction, classification, draftingKnown multi-step processesOpen-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.

lib/data.ts
// 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" },
      ]
    : [];
}
workflow.ts
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:

agent.ts
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

  1. Write six support emails: three billing questions, two technical, one unclear.
  2. Run both workflow.ts and agent.ts on each, recording the number of model calls, total tokens (from usage), and latency.
  3. 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

What is the defining difference between a workflow and an agent?
A compliance process must always check identity, then risk score, then approve. What fits best?
When does an agent start to pay off?

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.