Agentic Workflows
Agentic Workflows
Sub-chapter 4 of AI/ML · LLMs that take actions
A model that talks is a chatbot. A model that acts - calls APIs, reads files, sends emails, triggers deploys - is an agent. The leap is small in code (a while loop and some tool definitions) and large in consequence (one bad tool call can delete production data).
This sub-chapter is the operating manual for agentic systems: how to design them, how to keep them safe, and how to know when an agent is actually the right answer.
Outline
- What an agent actually is - LLM + tools + loop
- The agent loop - pseudocode you'll write twenty times
- Designing tools - schema, side effects, idempotency
- Multi-step planning - ReAct, plan-then-act, reflection
- Bounding the loop - iterations, budgets, escape hatches
- Safety patterns - confirmation, dry-run, audit trail
- Subagents and parallelism - when the work is independent
- State and memory - what the agent remembers between steps
- When NOT to build an agent - workflows that don't need one
- Production: tracing, evals, failure modes
101 Primer
What an agent actually is
A simple definition:
An agent is an LLM that, in a loop, decides which tool to call next based on the result of the previous tool call, and returns a final answer when it's done.
That's it. The model has tools, the tools have schemas, the runtime gives results back, the model decides what's next. The intelligence is in the decision loop.
The agent loop, in code
def run_agent(user_input, tools, max_iters=10):
messages = [{"role": "user", "content": user_input}]
for i in range(max_iters):
response = client.messages.create(
model="claude-sonnet-4-5",
system=SYSTEM_PROMPT,
tools=tool_schemas(tools),
messages=messages,
)
if response.stop_reason == "end_turn":
return response.content[-1].text
# Otherwise the model wants to call tools.
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type == "tool_use":
try:
result = tools[block.name].execute(**block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result),
})
except Exception as e:
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": f"Error: {e}",
"is_error": True,
})
messages.append({"role": "user", "content": tool_results})
raise RuntimeError("Agent exceeded max iterations")
You'll write a version of this many times. Eventually, prefer the Claude Agent SDK (Chapter VII) - it handles this loop, plus streaming, plus memory, plus subagents, plus context compaction.
Designing tools
A tool is a function the model can call. Its design is your agent's behaviour.
A well-designed tool has:
- A name in
snake_casethat reads like an English verb-phrase:search_customer,create_ticket,send_email. - A description that says when to use it, not what it does. The model reads this to decide.
- A typed input schema (JSON Schema). Required vs optional, enum values, min/max constraints.
- A typed output. Even if you serialise to string, document what it returns.
- No surprising side effects. A tool called
lookup_customermust not modify state. - Idempotency where possible. If the model retries a
create_ticketcall due to a transient error, two tickets is a bug. Use idempotency keys.
Example:
def create_ticket(
*,
customer_id: str,
title: str,
description: str,
priority: Literal["low", "medium", "high"],
idempotency_key: str, # the model is told to generate a UUID
) -> dict:
"""Create a support ticket. Use only when the customer has explicitly asked
for help and the issue is not already in an open ticket."""
...
The description does double duty: tells the model when to call it, tells the human reviewer what the agent will do.
Multi-step planning patterns
Three patterns, increasing in sophistication:
- Reactive (ReAct) - the loop above. The model observes, thinks, acts, observes again. Most agents are this.
- Plan-then-act - the model first produces a plan (numbered steps), then executes step by step. Better for multi-step workflows where the order matters. You can review the plan before executing.
- Plan + reflect - after each step, the model reflects on whether the plan still makes sense. Best for ambiguous tasks. Cost goes up.
For most product features, reactive is enough. For "do this 5-step thing on a customer's behalf," plan-then-act with a human-confirmable plan is the right shape.
Bounding the loop
The cardinal rule: always bound your agent loop.
- Max iterations - hard limit, e.g. 10. Better to fail with "agent gave up" than burn budget chasing its tail.
- Token budget - track cumulative tokens. Halt when budget is exhausted.
- Time budget - wall-clock timeout. A 60-second agent invocation should not become 10 minutes because of a retry storm.
- Escape hatches - give the model an explicit
give_up(reason)tool. It will use it. The reason is gold for debugging.
Safety patterns
These are not optional. Every production agent should have them.
-
Two-tier tool taxonomy. Safe tools (read-only, idempotent) are auto-executed. Dangerous tools (writes, sends, deletes, costs money) require explicit confirmation - either a human in the loop or a checked-in policy ("can spend up to $X without confirmation").
-
Dry-run mode. Every dangerous tool should support
dry_run=Truethat returns "I would have done X" without doing X. Use it in evals and for human review. -
Audit trail. Log every tool call with full inputs, outputs, and the model's reasoning (if you can capture it). Make this queryable. "Why did the agent send that email?" should be answerable in 30 seconds.
-
Sandboxing. File system tools? Restrict to a working directory. Shell tools? Whitelist commands or run in a container. Network tools? Allowlist domains.
-
Prompt injection resistance. Tools that read untrusted content (emails, scraped pages, RAG chunks) are injection vectors. Never let the model auto-execute tool calls produced from such content without re-confirming intent. Treat retrieved text as data, not instructions.
-
Rate limits and circuit breakers. If your agent triggers 50 API calls in 30 seconds, something is wrong. Cap it. Open a circuit. Alert.
Subagents and parallelism
When the work has independent parallel branches, dispatch subagents. Each gets a focused subtask and its own context.
main agent: "Compare these 3 customer accounts."
├── subagent: analyze customer A
├── subagent: analyze customer B
└── subagent: analyze customer C
main agent: synthesise the three reports
Wins:
- Parallelism - 3× wall-clock speedup.
- Context isolation - each subagent's context is clean, no cross-contamination.
- Cheaper synthesis - main agent only sees summaries, not raw work.
The Claude Agent SDK supports this natively (subagent_type=..., parallel dispatch). Roll-your-own is a thread pool + a clean message reset.
State and memory
What an agent should remember:
- Across steps in one task - the message history is the memory. Trim/compact when it grows.
- Across sessions - persist user-level facts (preferences, account info, prior context). Claude Code's memory system, or your own DB-backed key-value.
- Across users - usually a no. Tenant isolation always wins over agent convenience.
Three traps:
- Stuffing everything in context. Long context = slow, costly, sometimes worse quality. Use summaries + retrieval.
- Memory drift. A note saved six months ago may be wrong now. Add timestamps; let the model judge staleness.
- Memory as identity. "The agent remembers me!" feels magical, but it's also a privacy surface. Be explicit with users about what's stored.
When NOT to build an agent
A surprising fraction of "agentic" features should not be agents. Heuristics for not building one:
- The workflow is fixed and deterministic. "Read email → extract X → write to DB." That's an extraction pipeline. Single LLM call + parser. No loop.
- You can enumerate the steps in advance. A workflow engine (Temporal, Inngest) with LLM calls at certain nodes is more debuggable and cheaper.
- The decisions are high-stakes and audited. Don't have an agent decide what credit limit to extend. Have a model recommend and a deterministic rule decide.
- Latency requirements are tight. Agents are slow. If sub-second matters, don't.
Reach for the agent abstraction when (a) the path genuinely depends on intermediate results and (b) you can't enumerate the branches.
Production: tracing, evals, failure modes
- Trace every agent run end-to-end - Langfuse, LangSmith, Helicone, or your own. Capture: inputs, every tool call, every model response, the final answer, latency, token counts.
- Replay traces - being able to re-run a failed agent run against a new prompt or new tool definition is a superpower.
- Eval suite for agents - golden tasks (input → expected outcome / tool calls / final answer). Run on every change.
- Failure modes to watch for:
- Loops - agent calls the same tool with the same args repeatedly. Detect and bail.
- Hallucinated tool calls - wrong argument types, made-up tool names. Schema validation kills this at the boundary.
- Premature confidence - agent declares done while the task is half-finished. Stronger system prompt + a
verifystep. - Cost runaway - long inputs, repeated retries. Budget caps catch it.
Hands-on Checkpoints
- Build the agent loop above in Python or TS, from scratch. Two tools:
search_customer(safe) andcreate_ticket(dangerous; requiresconfirm: bool=False). - Run it on 5 sample inputs. Observe what it does. Where does it succeed? Where does it falter?
- Add bounded iterations + a
give_uptool. Trigger the bound on an impossible task. Confirm the failure mode is clean. - Add an audit log (one line per tool call). Make it queryable.
- Build a tiny eval suite: 5 input → expected outcome pairs. Run before/after a prompt change.
- Refactor to use the Claude Agent SDK. Note what you got for free.
- Add a subagent step: parallel analysis of 3 documents, then synthesis.
- Document the agent's safety properties (tools, confirmations, sandboxing) in a 1-page README. If you can't, the agent isn't safe enough to ship.
Further reading
- Anthropic - Building effective agents - the foundational read
- Anthropic - Claude Agent SDK - the SDK we use
- Lilian Weng - LLM agents - the deeper survey
- Simon Willison - Tools - running notes on tool design
- Cognition - Don't build multi-agents - counterpoint worth reading
Welzin opinion: Most "agentic" features should be workflows. Reach for an agent only when the decision tree is too dynamic to write. When you do, sweat the safety patterns - confirmations, dry-runs, audit, sandboxing - like your customers' data depends on them. It does.