Not every AI feature needs an agent
Short answer
Use a workflow when you already know the steps. Let code handle selection, validation and execution, and let the model handle the parts that need language understanding. Use an agent when the next step genuinely depends on what it discovers.
These days almost every LLM integration gets called an agent. Add a model, give it two tools, and suddenly it's "agentic".
I build a WhatsApp assistant for myself, and it is deliberately not an agent. The design is a workflow: plain code decides what the model sees, the model gets one narrow job, and code checks its answer. That choice makes the behaviour easier for me to inspect.
Here's how I think about the difference, and the same task built both ways so you can see what changes.
Workflow or agent, in one line each
- Single model callclassify, summarise, extract
- Retrieval, then generationfind the text, then write
- Workflowa known sequence
- Agentsteps unknown up front
- Several agentsrare: separate jobs
Five shapes an AI feature can take, grouped by who decides the steps. Your code decides in a single model call, in retrieval then generation, and in a workflow. The model decides in an agent and in several agents.
Anthropic's foundational guide to building effective agents, published in December 2024, draws a useful distinction. In a workflow, the model and tools run through code paths you define. In an agent, the model directs its own next steps and decides which tools to use.
The guide's practical advice is to start with the simplest approach and add complexity only when the problem needs it. Sometimes that means no agent at all.
Five shapes an AI feature can take
| Shape | Who controls the steps | Good for |
|---|---|---|
| Single model call | Your code | Classifying, summarising, rewriting or extracting from text you already have |
| Retrieval, then generation | Your code | Finding relevant document passages before asking the model to answer |
| Workflow | Your code | A known sequence: rank, call the model, validate and apply business rules |
| Agent | Mostly the model, within code defined tools and limits | Problems where the next step depends on what the model discovers |
| Several agents | Models work across separate roles, within an orchestration layer | Work that genuinely splits into independent jobs with clear boundaries |
These aren't levels you have to climb. They're different ways of organising an AI feature. Handing more decisions to the model can mean more requests, more accumulated context and a less predictable run. It depends on the design; a model can also return more than one tool call in a single response.
- Every chatindexed on my machine
- Rankingbehind a switch; unread chats otherwise
- Short listat most 50 chats, 40,000 tokens
- One model callGroq, OpenAI as fallback
- Quote checkplain code
- Focusverified only; the rest wait
Every chat is indexed on my machine. Plain code picks a short list, ranked by points when the switch is on and from unread chats otherwise. One model call reads that list and returns findings with quotes, plain code checks every quote against its message, and only verified findings reach Focus.
Message Yourself OS reads my WhatsApp and tells me who's waiting on me and what I promised. Every chat is indexed on my machine. In the ranked mode I'm testing, the flow looks like this:
every chat, indexed on my machine
|
v
plain code scores each chat on simple signals
(unread messages, they wrote last, a message ending in "?",
my last message using words like "I'll" or "I will",
activity today; muted chats and quiet groups lose points)
|
v
12 points to qualify, at most 50 chats, 40,000 token budget
|
v
one model call: what's waiting on me, what I promised,
each with the exact quote
|
v
plain code checks every quote against its message
|
v
!today: checked findings go into Focus, the rest are marked to verify
One detail matters: the ranked mode is behind a switch. Until it passes a gate of seven reviewed days of real use, the assistant uses unread chats instead. The diagram and comparison below show the ranked workflow I'm testing, not a claim that the new ranked brief is already the default.
The model never decides which chats to open. Code does, with rules I can read and explain. Those rules don't understand meaning; they look for signals like a question mark or the words "I'll" and "I will". Understanding whether a message is really a question for me or a promise is the model's job. Code then checks the evidence and sender.
One thing to be clear about, because this is WhatsApp: indexing and scoring happen on my machine, but the messages in the chats that make the shortlist do go to the model provider. The bot now uses Groq, with OpenAI as fallback. Local indexing means the model sees a shortlist, not that it sees nothing.
I should also be honest about the plan the bot used. Until November 2026 it ran on Gemini's unpaid quota. Google's terms say content sent through unpaid services can be used to improve Google products and may be reviewed by people, and they ask you not to submit sensitive, confidential or personal information. My shortlist contains other people's messages, so I've switched the bot's default provider to Groq. Groq's documentation says it doesn't keep inference inputs or outputs by default, may keep logs for up to 30 days when it investigates reliability or abuse, and lets every customer turn on Zero Data Retention to stop even that, which is how the bot's account is set. Read the terms for every provider in your own fallback chain before sending real messages; mine still has OpenAI as a fallback. The public demo below uses only made up chats. Google's Gemini API terms and Groq's data controls explain the details.
Could an agent do this? Sure. Give it list_chats and read_chat and let it figure things out. But I already know the steps. For this task, asking the model to rediscover them each morning adds calls and more places for the run to stop or go wrong.
The same task, built both ways
- 12 chatsmade up
- Rank in code7 qualify
- 1 model callJSON schema
- Quote checkin code
- 2 of 2 promisesevery run
- list_chatsnames, no text
- read_chatRiya, Mom, standup, usually the recruiter
- finishits own call
- Quote checkthe same check
- 0 of 2 promisesArjun and Neha never opened
Both start from the same twelve made up chats. The workflow ranks them in code, makes one model call on the seven that qualify, checks every quote and finds both promises. The agent lists the chats, reads the ones where someone asked a question, decides to finish, passes the same check and finds neither promise, because it never opened Arjun's or Neha's chats.
To make the difference concrete I built a small demo: twelve made up chats, and the same triage task done twice. The code, prompts, settings and raw traces are on GitHub.
The workflow ranks chats with the same point rules, sends only the ones that qualify, makes one model call with a JSON schema, and checks the quotes in code:
const ranked = pickCandidates(chats, today);
const reply = await model.generate({
system,
turns: [{ role: "user", text: prompt }],
jsonSchema: findingSchema,
});
const findings = verifyFindings(parseFindings(reply.text), chats);
The agent gets three tools (list_chats, read_chat, finish) and a loop. It decides what to read, one step at a time, until it calls finish or hits 15 steps:
for (let step = 1; step <= maxSteps; step++) {
const reply = await model.generate({ system, turns, tools: agentTools });
const finish = reply.toolCalls.find((call) => call.name === "finish");
if (finish) return verifyFindings(finish.args.findings, chats);
for (const call of reply.toolCalls) {
turns.push({
role: "tool",
name: call.name,
result: runTool(call.name, call.args, chats),
});
}
}
Both end with the same check, so the comparison is fair in one important sense: same chats, same model and same rules for checking evidence. For each finding, the check confirms that the quote appears in the named message and that the sender matches the claim: a promise has to be a message I sent; "waiting on me" has to be a message someone else sent. That verifies evidence and attribution. It doesn't prove the model understood the meaning correctly.
What the traces showed
I ran the comparison three times on 10 November 2026, on openai/gpt-oss-120b through Groq. I started with gemini-2.5-flash, but my AI Studio project showed a daily limit of 20 requests for that model. I kept the Gemini run separate from the three Groq runs rather than mixing the numbers. It is in the repo too and showed the same pattern.
These are the three Groq runs. Ranges go from the lowest to the highest run; findings show each run's result.
| Measure | Workflow | Agent |
|---|---|---|
| Model calls | 1 | 5 to 6 |
Tools run (finish not counted) |
0 | 4 to 5 |
| Input tokens, all calls added up | 878 | 3,633 to 4,695 |
| Visible output tokens, reasoning excluded | 299 to 503 | 377 to 464 |
| Thinking tokens | 435 to 723 | 613 to 1,029 |
| Model time, not counting waits | 2.7 to 3.5 s | 5.9 to 8.2 s |
| Waits after an HTTP 429 response | 0 to 10 s | 0 to 29 s |
| Findings that passed the evidence check | 8, 7, 7 | 4, 5, 5 |
| Promises found, out of 2 | 2 every run | 0 every run |
Visible output tokens and thinking tokens are separate rows in these traces. Groq reports thinking as part of completion tokens; the demo subtracts it to show visible output and reasoning separately. Add the two rows to get the completion token total reported by the provider.
This is three runs of two implementations on one synthetic dataset. It isn't a benchmark. A better agent prompt, different stopping conditions, another model or another task could change the result. Treat these as a few honest data points, not a law.
In this implementation, each agent step makes another model request and sends the conversation so far again. That is why its input token count grows as it opens chats, while the workflow sends its shortlist once.
Added up across its calls, the agent's input came to about 4.1 to 5.3 times the workflow's for the same twelve chats. That number is cumulative across requests, not one large context window. Every request also counts against the provider's rate limits. The demo waits only when a provider returns HTTP 429, uses the delay the provider supplies and records it. In these back to back Groq runs, the agent waited up to 29 seconds; the workflow waited once, for 10 seconds. In the Gemini run, my project showed a five requests per minute limit for that model, and the agent's sixth request waited 94 seconds. Those are the limits I saw on my accounts that day, not fixed limits for everyone. Groq's documentation explains that its limits vary by model and account: Groq rate limits. Google's quota documentation works the same way: Gemini API rate limits.
The bigger surprise was what it found. In every run the agent listed the chats, opened Riya, Mom, the team standup and usually the recruiter, all chats where someone asked me something, and then decided it was done. It wasn't forced to read every chat. It could call finish when it thought it had enough, within the 15 step limit. That's the decision I had handed to the model, and it stopped before reaching the two chats that mattered. It never opened Arjun's or Neha's chats, where I had made promises. So it missed both promises in all three Groq runs and in the Gemini run. On Gemini it also labelled the standup message "can you share the deploy notes before 4?" as a promise I made. The quote check caught that because the message wasn't sent by me.
The workflow found both promises in every run because my ranking gives my last message containing "I'll" or "I will" the same 14 points as a message ending in a question mark. Both chats qualified, so the model saw them. The rule is in code; it doesn't depend on the model remembering to look.
The check has limits too. In one run the workflow listed Arjun's "Bro the API docs link is broken" as waiting on me. The quote is real and Arjun sent it, so it passed the evidence check even though it isn't really a question. Code can check evidence; it can't check judgement.
Aur yahi interesting part hai. The agent isn't wrong for every problem. It was the wrong fit for this one: it spent model calls deciding which chats to inspect even though my application already had a rule for that.
What this experiment doesn't prove
- Rules in codeyou can read them
- No promise points“kal tak bhej dunga”
- May never reach the modelnothing later brings it back
- Model decideswhat to open, when to stop
- Stops earlycalls finish on its own
- Arjun and Nehanever opened
A workflow fails in its rules: a promise written as kal tak bhej dunga earns no promise points, so that chat may never reach the model. An agent fails in its choices: it decides when it has read enough, stops early and never opens the chats that mattered.
A workflow can fail too. If my scoring rules are wrong, a chat that matters never reaches the model, and nothing downstream can bring it back. Someone who writes "kal tak bhej dunga" instead of "I'll send it tomorrow" scores no promise points in my rules. An agent can adapt to things my rules never anticipated.
That's the real tradeoff:
- Workflow: you encode more of your assumptions in code.
- Agent: you hand more decisions to the model.
Neither removes failure. They move where the failure happens. For my inbox I'd rather have failures I can find by reading my own rules than failures I have to dig out of a trace.
Where the difference really shows up
| Workflow | Agent | |
|---|---|---|
| Model calls | Fixed for this design: one | One per step in this loop; the count depends on the run |
| Tokens | Predictable within the input budget | Grow as the conversation is sent again on each step |
| Control flow | Defined by code | Directed by the model within the tools and limits you expose |
| Next action | Defined by code | Chosen at run time |
| Model output | Can vary | Can vary |
| Debugging | Read the code path and model result | Read the trace and work out why it chose that step |
| Tool permissions | Code calls only what it needs | The model can call any tool you expose, so expose less |
| When it fails | Easier to locate within a known sequence | Can stop early or keep going; set step limits |
| Human checks | Easy to place at fixed points | Put gates around the actions that matter |
When I would use an agent
When I can't write the steps down. Investigating an unfamiliar codebase, research where each answer decides the next question, or debugging when I don't know what I'll find. That's where letting the model choose its next move can earn its cost.
Even then, I'd keep the parts I do know in code: permissions, validation and the final check. The model proposes; my application decides what actually happens.
What I'd tell someone building an AI feature
- Write down the steps first. If you can, you probably want a workflow.
- Give the model the smallest job only it can do, and let code do the rest.
- Check its output in code, not with another prompt.
- Add agency only where the steps are truly unknown, and put a step limit on it.
An agent isn't a better kind of software. Sometimes giving the model fewer decisions is exactly what makes the product good.
Sources
Written by Sayam Jain, AI Product Engineer in Bengaluru. More about me