Agentic AI Workflows: Patterns and Pitfalls
Agents that use tools, plan, and take multi-step actions are powerful and fragile. This post covers the core patterns, the failure modes to design around, and what production-ready agents actually look like.
What an AI Agent Is
An AI agent is an LLM that can take actions - calling tools, searching the web, writing and executing code, reading and writing files - in service of completing a task. Unlike a single prompt → response, an agent runs a loop: think → act → observe result → think again → act again.
This loop makes agents capable of multi-step tasks that a single LLM call cannot complete. It also introduces new failure modes.
The ReAct Pattern
The most common and robust agent pattern is ReAct (Reason + Act): the model alternates between reasoning about what to do and taking an action.
pythonimport anthropic import json client = anthropic.Anthropic() # Define available tools tools = [ { "name": "search_web", "description": "Search the web for current information on a topic", "input_schema": { "type": "object", "properties": { "query": {"type": "string", "description": "Search query"} }, "required": ["query"] } }, { "name": "calculate", "description": "Evaluate a mathematical expression", "input_schema": { "type": "object", "properties": { "expression": {"type": "string", "description": "Math expression to evaluate"} }, "required": ["expression"] } } ] def execute_tool(tool_name: str, tool_input: dict) -> str: if tool_name == "search_web": # In production: call a real search API return f"[Search results for '{tool_input['query']}': ...]" elif tool_name == "calculate": try: result = eval(tool_input["expression"]) return str(result) except Exception as e: return f"Error: {e}" return "Unknown tool" def run_agent(task: str, max_turns: int = 10) -> str: messages = [{"role": "user", "content": task}] for turn in range(max_turns): response = client.messages.create( model="claude-sonnet-4-6", max_tokens=2048, tools=tools, messages=messages, ) # Append assistant response messages.append({"role": "assistant", "content": response.content}) # Check if agent is done (no tool use) if response.stop_reason == "end_turn": return next( (block.text for block in response.content if hasattr(block, "text")), "No text response" ) # Execute tool calls tool_results = [] for block in response.content: if block.type == "tool_use": result = execute_tool(block.name, block.input) tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": result, }) messages.append({"role": "user", "content": tool_results}) return "Max turns reached without completing task"
The Core Pitfalls
Unbounded Loops
Without a turn limit, a confused agent loops indefinitely. Always set max_turns. Add explicit loop detection.
Cascading Errors
Error in step 3 → step 4 uses wrong information → step 5 compounds the error. By step 8, you have confident nonsense. Solutions:
- Validate tool outputs before passing to the next step
- Use structured output for intermediate steps so validation is possible
- Consider checkpointing: save state after each successful step and retry from the checkpoint on error
Irreversible Actions
An agent that can delete files, send emails, or modify databases must be constrained. Principle: make the agent propose actions, require human confirmation for irreversible ones.
pythondef safe_execute_tool(tool_name: str, tool_input: dict, require_confirmation: bool = True) -> str: DANGEROUS_TOOLS = {"delete_file", "send_email", "update_database"} if tool_name in DANGEROUS_TOOLS and require_confirmation: print(f"\n[CONFIRMATION REQUIRED] Tool: {tool_name}, Input: {tool_input}") if input("Proceed? (yes/no): ").lower() != "yes": return "Action cancelled by user" return execute_tool(tool_name, tool_input)
Prompt Injection
User input passed to an agent can contain adversarial instructions. An agent that processes web content or user-submitted documents is at risk of prompt injection - the malicious content redirects the agent to take unintended actions.
Mitigations:
- Separate user data from system instructions with clear delimiters
- Validate tool calls against an explicit allowlist
- Run agents with minimum necessary permissions
Injection got worse as agents got more capable, and the industry has largely stopped pretending there is a clean fix. The model reads its instructions and the content it retrieved as one token sequence; there is no privilege boundary inside that sequence. So the defense is not "detect the injection", it is "limit what a successful injection can do": least-privilege tools per task, sandboxed execution, human confirmation before irreversible actions, and treating anything the agent reads (web pages, emails, documents, tool output) as data rather than instructions. The OWASP Top 10 for Agentic Applications is the current canon for this; the categories to know are prompt injection, insecure tool execution, excessive agency, and memory poisoning.
Planning-Based Agents
For complex multi-step tasks, a separate planning step before execution improves reliability:
pythondef plan_and_execute(task: str) -> str: # Step 1: Generate a plan plan_response = client.messages.create( model="claude-sonnet-4-6", max_tokens=1024, messages=[{ "role": "user", "content": f"Create a step-by-step plan to complete this task: {task}\nList only the steps, not the execution." }] ) plan = plan_response.content[0].text # Step 2: Execute the plan with the agent return run_agent(f"Execute this plan:\n{plan}\n\nOriginal task: {task}")
The Harness Is the Product
Every pitfall above has the same shape: the model did something the system around it should not have allowed. That system has a name now, the harness, and "harness engineering" is the discipline of building it. The formula people use is agent = model + harness. The model plans and proposes; the harness validates, authorizes, executes, and logs. Unbounded loops are a missing turn budget in the harness. Cascading errors are a missing verification step. Irreversible actions are a missing approval gate. Prompt injection is a missing permission boundary.
The consequence for how you spend your time: when an agent fails, resist the reflex to rewrite the prompt. Ask which harness layer should have caught it, fix that layer, and add the failure to your eval suite so it stays fixed. Teams that do this converge on reliable agents. Teams that iterate on prompts alone converge on a very long prompt.
What Production Agents Actually Need
Beyond the basic loop:
- Logging: Every tool call and result, for debugging and compliance
- Timeout per turn: Individual tool calls can hang; set per-call timeouts
- State persistence: For long-running agents, save state to a database so they can resume
- Human-in-the-loop checkpoints: For high-stakes decisions, pause and require human review
- Evaluation: Track task completion rate, error rate, and number of turns per task
Agents are more complex to operate than simple LLM calls. Build the observability infrastructure before scaling them.
Stay in the loop
Get new ML/AI lessons in your inbox.
No account needed. We will send curriculum updates, launch notes, and practical learning resources.
Related Posts
More postsAgentic Coding: Working With Claude Code, Codex, and Cursor
Coding agents are now the default way software gets written. Learn the gather-act-verify loop, how to write CLAUDE.md and AGENTS.md files that actually steer an agent, when to use skills and subagents, and how to review agent output like a senior engineer.
Context Engineering: Designing What the Model Sees
The context window is a budget, and everything competes for it: the system prompt, the tool list, retrieved documents, memory, and the conversation so far. Learn to design the context deliberately, scope tools per task, compact without losing what matters, and treat cache hit rate as the metric it has become.
Harness Engineering: The Runtime Around the Model
Agent = model + harness. The harness is the deterministic runtime that validates, authorizes, executes, and logs every action the model proposes. Learn its five layers, build one from scratch, and adopt the loop that turns every agent failure into a permanent fix.