teachyou.ai academy
← All posts
Workflow AutomationAI agentsLLM orchestrationtool usemulti-agent systems

AI Agent Workflow Patterns for Automation

Pramod Dutta · Jul 8, 2026 · 13 min read

AI agent workflow patterns are the reusable structures you assemble to make a language model reliably finish multi-step work: prompt chaining, routing, parallelization, orchestrator-worker, evaluator-optimizer, and full autonomous agent loops. If you are building automation with an LLM, picking the right pattern (or the right combination) matters more than picking the fanciest model. This article walks through each pattern with working code, tells you when to use it, and covers the failure modes nobody puts in the marketing deck.

Most teams start with a single giant prompt and a hope. That works for demos. It falls apart the moment the task has more than two or three decision points, because a single call has no way to check its own work, retry a failed step, or split load across specialized sub-tasks. The patterns below exist because someone hit that wall and needed a structural fix, not a longer prompt.

Why workflow patterns beat one big prompt

A single monolithic prompt asks the model to plan, execute, and verify all at once, inside one context window, with no checkpoints. Three things go wrong at scale:

  • Error compounding. If step 3 of a 10-step task is wrong, nothing downstream catches it, because there is no downstream, there is one shot.
  • Context bloat. Long instructions covering every edge case degrade instruction-following. Models pay less attention to a rule buried at line 200 than one at line 20.
  • No parallelism. A single call is inherently serial. Independent sub-tasks that could run at once end up queued behind each other.

Workflow patterns fix this by giving the LLM call boundaries: explicit steps with defined inputs and outputs, so you can inspect, retry, cache, and parallelize at each boundary. This is the same reason we don't write entire applications as one function. The distinction that matters here, borrowed from how Anthropic frames it, is workflows versus agents. A workflow is a predefined sequence of LLM and tool calls that you, the developer, wire together in code. An agent is a loop where the LLM itself decides what to call next based on the results so far. Both are legitimate "AI agent workflow patterns" in casual usage, but they have very different reliability and cost profiles, and conflating them is the number one reason automation projects miss their deadline.

Pattern 1: Prompt chaining

Prompt chaining breaks a task into an ordered sequence of LLM calls, where the output of one call becomes the input to the next. Add a validation gate between steps and you can stop the chain early if something looks wrong, instead of paying for downstream calls on bad data.

Use this when the task decomposes naturally into fixed stages, such as "draft an outline, then write the doc, then translate it." Use it, in particular, when you can write a cheap, deterministic check between two of those stages, that's where the pattern earns its keep over a single prompt.

from anthropic import Anthropic

client = Anthropic()

def call(prompt: str, system: str = "") -> str:
    msg = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=1024,
        system=system,
        messages=[{"role": "user", "content": prompt}],
    )
    return msg.content[0].text

def draft_outline(topic: str) -> str:
    return call(f"Write a 5-bullet outline for a blog post about: {topic}")

def outline_is_valid(outline: str) -> bool:
    # cheap deterministic gate, no LLM call needed
    return outline.count("-") >= 5 or outline.count("\n") >= 5

def write_post(outline: str, topic: str) -> str:
    return call(
        f"Using this outline, write a full blog post about {topic}.\n\nOutline:\n{outline}"
    )

def chain(topic: str) -> str:
    outline = draft_outline(topic)
    if not outline_is_valid(outline):
        outline = draft_outline(topic)  # one retry
    return write_post(outline, topic)

result = chain("why caching matters in RAG pipelines")
print(result)

The gate function is doing real work here: it's a cheap string check, not another model call, so it costs nothing to run on every chain execution. That is the whole point of chaining, push verification as far left as possible.

Pattern 2: Routing

Routing classifies an incoming request and sends it down one of several specialized paths. This matters because a prompt tuned to handle refund requests well is usually worse at handling technical bug reports, and vice versa. One generalist prompt trying to do both ends up mediocre at each.

from anthropic import Anthropic
import json

client = Anthropic()

def route(user_message: str) -> str:
    msg = client.messages.create(
        model="claude-haiku-4-5",  # small, fast model for classification
        max_tokens=20,
        messages=[{
            "role": "user",
            "content": (
                "Classify this support message into exactly one category: "
                "billing, technical, or general.\n\n"
                f"Message: {user_message}\n\n"
                "Respond with only the category word."
            ),
        }],
    )
    return msg.content[0].text.strip().lower()

def handle_billing(msg: str) -> str:
    return call(msg, system="You are a billing specialist. Be precise about numbers and policy.")

def handle_technical(msg: str) -> str:
    return call(msg, system="You are a technical support engineer. Ask for logs and steps to reproduce.")

def handle_general(msg: str) -> str:
    return call(msg, system="You are a friendly, general support agent.")

def dispatch(user_message: str) -> str:
    category = route(user_message)
    handlers = {"billing": handle_billing, "technical": handle_technical, "general": handle_general}
    handler = handlers.get(category, handle_general)
    return handler(user_message)

Notice the small model for classification and a larger one for the actual response. That split is the most common cost optimization in production agent systems: routing is a cheap decision, the real work is expensive, don't pay full price for the cheap part.

Pattern 3: Parallelization

Parallelization runs multiple LLM calls at the same time and combines the results. There are two flavors worth distinguishing.

Sectioning splits a task into independent subtasks that run concurrently, like reviewing a pull request for security issues, performance issues, and style issues in three parallel calls instead of one sequential pass. Voting runs the same task multiple times with slightly different prompts or temperatures and aggregates the answers, useful when you need higher confidence on a judgment call, like content moderation.

import asyncio
from anthropic import AsyncAnthropic

client = AsyncAnthropic()

async def review_aspect(code: str, aspect: str) -> str:
    msg = await client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=512,
        messages=[{
            "role": "user",
            "content": f"Review this code for {aspect} issues only. Be specific.\n\n```\n{code}\n```",
        }],
    )
    return msg.content[0].text

async def parallel_review(code: str) -> dict:
    aspects = ["security", "performance", "readability"]
    results = await asyncio.gather(*(review_aspect(code, a) for a in aspects))
    return dict(zip(aspects, results))

# usage
# results = asyncio.run(parallel_review(source_code))

Sectioning cuts wall-clock time roughly by the number of parallel branches, since the branches don't wait on each other. It also isolates prompt complexity, each call only needs to reason about one dimension, which tends to produce sharper output than asking one call to juggle security, performance, and style simultaneously.

Pattern 4: Orchestrator-worker

In this pattern, a central orchestrator LLM call breaks a task into subtasks dynamically, at runtime, then dispatches each to a worker call and synthesizes the results. This differs from parallelization because the subtasks aren't fixed in code ahead of time, the orchestrator decides how many workers to spawn and what each should do based on the specific input.

from anthropic import Anthropic
import json

client = Anthropic()

def plan_subtasks(task: str) -> list[str]:
    msg = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=512,
        messages=[{
            "role": "user",
            "content": (
                f"Break this task into 2-5 independent subtasks. Task: {task}\n\n"
                "Respond with a JSON array of subtask strings only."
            ),
        }],
    )
    text = msg.content[0].text.strip()
    return json.loads(text)

def run_worker(subtask: str) -> str:
    msg = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=512,
        messages=[{"role": "user", "content": subtask}],
    )
    return msg.content[0].text

def synthesize(task: str, subtask_results: list[str]) -> str:
    joined = "\n\n".join(f"Result {i+1}: {r}" for i, r in enumerate(subtask_results))
    return call(f"Original task: {task}\n\nSubtask results:\n{joined}\n\nSynthesize a final answer.")

def orchestrate(task: str) -> str:
    subtasks = plan_subtasks(task)
    results = [run_worker(s) for s in subtasks]
    return synthesize(task, results)

This is the right pattern for research and analysis tasks where you don't know in advance how many angles the task needs, like "research three competitors and summarize their pricing strategy." A fixed sectioning pipeline can't handle that because the number of competitors is only known once the model reads the request.

Pattern 5: Evaluator-optimizer

One LLM call generates a response, a second evaluates it against explicit criteria, and if it fails, the generator retries with the evaluator's feedback folded in. This loop continues until the evaluator approves or a max-retry limit is hit.

from anthropic import Anthropic

client = Anthropic()

def generate(task: str, feedback: str = "") -> str:
    prompt = task if not feedback else f"{task}\n\nPrevious attempt feedback: {feedback}\nRevise accordingly."
    return call(prompt)

def evaluate(task: str, output: str) -> tuple[bool, str]:
    msg = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=256,
        messages=[{
            "role": "user",
            "content": (
                f"Task: {task}\nOutput: {output}\n\n"
                "Does this fully satisfy the task? Reply with PASS or FAIL on the first line, "
                "then one sentence of specific feedback."
            ),
        }],
    )
    text = msg.content[0].text
    passed = text.strip().upper().startswith("PASS")
    feedback = text.split("\n", 1)[1] if "\n" in text else ""
    return passed, feedback

def evaluator_optimizer(task: str, max_iters: int = 3) -> str:
    output = generate(task)
    for _ in range(max_iters):
        passed, feedback = evaluate(task, output)
        if passed:
            return output
        output = generate(task, feedback)
    return output  # return best effort after max_iters

This pattern is worth the extra latency and cost specifically when you have clear, checkable evaluation criteria and generation is genuinely hard to get right on the first pass, translation nuance, code that must pass specific constraints, or copy that needs to hit a tone guideline. If there's no crisp definition of "good," the evaluator just adds noise and cost.

Pattern 6: The autonomous agent loop

All the patterns above are workflows: you decide the control flow in code. An agent, by contrast, runs in a loop where the LLM itself picks the next tool call, observes the result, and decides whether to continue or stop. This is the pattern behind coding agents, computer-use agents, and most "agentic" automation you've seen demoed.

from anthropic import Anthropic

client = Anthropic()

tools = [
    {
        "name": "search_docs",
        "description": "Search internal documentation for a query",
        "input_schema": {
            "type": "object",
            "properties": {"query": {"type": "string"}},
            "required": ["query"],
        },
    },
    {
        "name": "send_email",
        "description": "Send an email to a recipient",
        "input_schema": {
            "type": "object",
            "properties": {
                "to": {"type": "string"},
                "body": {"type": "string"},
            },
            "required": ["to", "body"],
        },
    },
]

def execute_tool(name: str, tool_input: dict) -> str:
    if name == "search_docs":
        return f"Found 3 relevant docs for '{tool_input['query']}'"
    if name == "send_email":
        return f"Email sent to {tool_input['to']}"
    return "Unknown tool"

def agent_loop(user_task: str, max_steps: int = 8) -> str:
    messages = [{"role": "user", "content": user_task}]
    for step in range(max_steps):
        response = client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=1024,
            tools=tools,
            messages=messages,
        )
        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason != "tool_use":
            return "".join(b.text for b in response.content if b.type == "text")

        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 steps reached without completion."

The max_steps guard is not optional. An agent loop without a hard ceiling on iterations, or a budget on tokens and tool calls, can burn through cost with no output if it gets stuck retrying a failing tool call. Always cap it, and always log every tool call so you can debug a stuck loop after the fact.

Choosing the right pattern

A rough decision order that holds up across most automation projects:

  1. If the task is a fixed sequence of known steps, use prompt chaining.
  2. If different inputs need genuinely different handling, use routing.
  3. If subtasks are independent and known ahead of time, use parallelization.
  4. If the number and shape of subtasks depend on the input, use orchestrator-worker.
  5. If output quality benefits from a checkable review-and-revise cycle, add evaluator-optimizer, often layered on top of one of the above.
  6. Only reach for a full autonomous agent loop when the task genuinely requires open-ended tool selection and you cannot predict the steps in advance. It's the most flexible pattern and also the hardest to make reliable and the most expensive to run.

A common mistake is jumping straight to pattern 6 because it looks the most impressive. In practice, most business automation, invoice processing, support triage, content pipelines, fits cleanly into patterns 1 through 5, which are cheaper, faster, easier to test, and far easier to debug when something goes wrong at 2am. Reserve the open-ended agent loop for tasks where you truly cannot enumerate the steps ahead of time, like a coding agent that has to explore an unfamiliar codebase before it knows what to change.

Common failure modes to design around

  • Silent step failures. If a chain step returns malformed output and the next step doesn't validate it, garbage propagates all the way to the end. Add a schema check after every LLM call that produces structured data.
  • Unbounded loops. Every agent loop and every evaluator-optimizer retry needs a max iteration count and ideally a token budget, not just a step count.
  • Over-parallelizing dependent work. Sectioning only works when subtasks are truly independent. If worker B needs worker A's output, that's a chain, not a parallel branch, forcing it into parallel just introduces race conditions in your synthesis step.
  • No observability. Log the input, output, and latency of every call in every pattern. When a workflow misbehaves in production, you need to see which step produced the bad output, not just the final answer.
  • Using a big model everywhere. Classification, routing, and simple validation steps rarely need your most capable model. Route those to a smaller, faster model and save the expensive one for generation and synthesis steps where reasoning quality actually changes the outcome.

FAQ

What is the difference between an AI workflow and an AI agent? A workflow is a predefined sequence of LLM and tool calls wired together in your code, you control the path. An agent is a loop where the LLM decides at each step what to do next based on prior results, it controls the path. Workflows are more predictable and cheaper to run; agents are more flexible but harder to bound and debug.

Which pattern should I start with for a new automation project? Start with prompt chaining or routing. Both are the cheapest to build, test, and reason about. Move to parallelization, orchestrator-worker, or evaluator-optimizer only once you've confirmed the simpler pattern can't hit your accuracy or latency target.

Do I need a framework like LangChain or LangGraph to implement these patterns? No. Every pattern in this article is plain code calling an LLM API directly, no framework required. Frameworks add value once you have many workflows to manage and want shared tracing, retries, and state persistence, but they are not a prerequisite for building any single pattern.

How do I prevent an agent loop from running forever? Cap it on two axes: a max number of steps (a simple counter, as shown above) and a max token or cost budget tracked across the whole run. Also log every tool call so that if the loop does hit the cap, you can inspect the transcript and see exactly where it got stuck.

Can I combine multiple patterns in one system? Yes, and in practice most production systems do. A common combination is routing at the entry point, followed by a chain or orchestrator-worker for the matched path, with an evaluator-optimizer pass on the final output before it reaches the user. Treat each pattern as a composable building block rather than a competing architecture choice.

Is a bigger, more capable model a substitute for good workflow design? No. A stronger model reduces errors within a single call, but it does not give you retry logic, validation gates, or parallel execution. A well-structured workflow using a mid-tier model consistently outperforms a single unstructured call to a flagship model on multi-step tasks, because the structure is what catches and corrects errors, not raw model capability.