{"slug": "ai-introduction-part-3-workflow-patterns-part-1", "title": "AI Introduction Part 3: Workflow Patterns (Part 1)", "summary": "Anthropic distinguishes workflows from agents, defining workflows as systems where LLMs are used through predefined code paths, and recommends finding the simplest solution possible, only increasing complexity when needed. The article introduces five workflow patterns—prompt chaining, routing, parallelization, orchestration, and evaluator-optimizer—and details the first two, emphasizing the role of the stop_reason and tool_use in LLM orchestration, with a Python example using the Anthropic API to query invoices.", "body_md": "Anthropic defines an important difference between workflows and agents: Workflows, or workflow patterns, are systems where LLMs are exercised (used) through predefined code paths. Agents, or agentic systems, are systems in which LLMs direct their own processes and tool use.\n\nAnthropic is explicit about the fact that you might not need an agentic system: “We recommend finding the simplest solution possible, and only increasing complexity when needed. This might mean not building agentic systems at all.”\n\nNon-agentic systems that use LLMs to do some or most of their work are called workflows. They are workflow applications that leverage LLMs to perform specific tasks but aren’t actually driven by the LLM itself. (Much of this post has been adapted from the public whitepaper “[Building Effective Agents](https://www.anthropic.com/engineering/building-effective-agents),” with more detailed explanations and code examples in Python provided by me.)\n\nToday we will explore workflow patterns. These are five distinct types of architectures used in workflow systems:\n\nPrompt Chaining\n\nRouting\n\nParallelization (including sectioning & voting)\n\nOrchestration\n\nEvaluator-Optimizer\n\nThis post will cover Prompt Chaining & Routing. The *next* post will cover the other three patterns.\n\nFirst, let’s discuss what an “augmented LLM” is. Anthropic calls this the building block for all of the patterns we’re about to discuss. Unlike Claude web or ChatGPT web, all LLM APIs are stateless: they don’t remember anything that happened before. They always start fresh with each call.\n\nSometimes, as is the case with RAG (which will be covered in a later post), you give the LLM the full context it needs to generate a response. With RAG, specifically, you might give it 3 writing samples from already approved snippets of text. These guide the LLM to write in a voice that matches what you already have approved.\n\nIn other workflow patterns (the ones we will discuss today), the LLM responds not with a complete response, but with an almighty **stop_reason**. Stop reason is what makes LLM orchestration so powerful: it’s the LLM’s way of saying “I’ve determined I don’t have the full context for completing the task, so I’m going to stop here and kick this problem back to you, telling you why I stopped.”\n\nWhen you make the request, you tell the API what kinds of tools you can provide it during this process. Unlike web AI interfaces, which have a harness that handles this behind a curtain, LLM APIs don’t and actually can’t use tools on their own. They can’t go look anything up. They can’t make external queries to get, for example, today’s weather.\n\nInstead, you can provide that as a tool back to the API. It’s a little tricky.\n\nThe main reason you will see **stop_reason** when designing an LLM orchestration is **tool_use**.\n\nA good example of this is when we ** can’t** know in advance what the LLM might ask for. (IF we did know ahead of time, we’d pre-fetch that information and pass it into the prompt.)\n\nAssume the CEO prompts, “Which of my invoices are overdue and who owes the most?”\n\nThe model reads it, determines it needs data, and issues a tool_use request such as query_invoices(status: “overdue”, sort: “amount_desc”). Your code runs that against your actual database, returns the rows, and the model writes the summary.\n\nYou couldn’t have passed the invoices in up front. There are ten thousand of them, and you don’t know which slice matters until the model parses the question. That’s the point of the loop: the model decides what it needs based on the request, and only your code can fetch it.\n\nIn the classic client-server diagram, it might look like this.\n\nFirst, you declare the tool and send the user’s question:\n\n``` python\nimport anthropic\n\nclient = anthropic.Anthropic()\n\ntools = [\n\n    {\n\n        \"name\": \"query_invoices\",\n\n        \"description\": \"Fetch invoices from the database by status and sort by amount due.\",\n\n        \"input_schema\": {\n\n            \"type\": \"object\",\n\n            \"properties\": {\n\n                \"status\": {\"type\": \"string\"},\n\n\"sort\": {\"type\": \"string\"},\n\n            },\n\n            \"required\": [\"status\"],\n\n        },\n\n    }\n\n]\n\nmessages = [\n\n    {\"role\": \"user\", \"content\": \"Which of my invoices are overdue and who owes the most?\"}\n\n]\n\nresponse = client.messages.create(\n\n    model=\"claude-sonnet-4-6\",\n\n    max_tokens=1024,\n\n    tools=tools,\n\n    messages=messages,\n\n)\n```\n\nNow look at what comes back. The model didn’t answer. It stopped and asked for the tool:\n\n```\nprint(response.stop_reason)\n\n# \"tool_use\"\nprint(response.content)\n# [\n#   TextBlock(text=\"Let me check your overdue invoices.\"),\n#   ToolUseBlock(\n#       id=\"toolu_01A...\",\n#       name=\"query_invoices\",\n#       input={\"status\": \"overdue\", \"sort\": \"amount_desc\" }\n#   )\n# ]\n```\n\nThen, you run the query the LLM asked for in your own database.\n\n```\nstop_reason is tool_use, and the model handed you a request with the arguments it wants. It has not touched your database. It can't.\n\ntool_use = response.content[-1]        # the ToolUseBlock\n\ndef query_invoices(status, sort=None):\n    # your real DB call goes here\n    return [\n        {\"id\": 1, \"amount\": 50},\n        {\"id\": 2, \"amount\": 150},\n    ]\nrows = query_invoices(**tool_use.input)\n```\n\nThen you send ** everything** back: the original question, the model’s tool-call turn, and a tool result matched to the request by id. Remember, you must send everything back because LLM APIs are fully stateless—they’re like talking to someone with perpetual amnesia. They won’t remember anything from the previous conversation.\n\n```\nmessages.append({\"role\": \"assistant\", \"content\": response.content})\n\nmessages.append({\n    \"role\": \"user\",\n    \"content\": [\n        {\n            \"type\": \"tool_result\",\n            \"tool_use_id\": tool_use.id,\n            \"content\": str(rows),\n        }\n    ],\n})\n\nfinal = client.messages.create(\n    model=\"claude-sonnet-4-6\",\n    max_tokens=1024,\n    tools=tools,\n    messages=messages,\n)\n```\n\nNow the model gives the real answer, along with a **stop_reason** of **end_turn**, which means the loop is done.\n\n```\nprint(final.stop_reason)\n\n# \"end_turn\"\n\nprint(final.content[0].text)\n\n# \"You have 2 overdue invoices totaling $200. Invoice 2 is the largest at $150.\"\n```\n\nThe two create calls are the two right-pointing arrows in the diagram. The tool_use response and your tool_result message are the middle of the loop. end_turn is the model saying it’s done and needs nothing more from you.\n\nIf the model needed a second tool call, final.stop_reason would be tool_use again instead of end_turn. That’s why real code wraps this in a while loop that keeps going until the stop reason is no longer tool_use.\n\nNow that we understand the building block pattern, let’s look at the five distinct workflow patterns.\n\n# Prompt Chaining\n\nPrompt chaining refers to a technique where we decompose a task into a sequence of steps. Each LLM call processes the output of the previous one. You can programmatically check any intermediate step to ensure that the process is still on track to produce a good result. (Those programmatic checks are called “gates.”)\n\n``` python\nimport anthropic\n\nclient = anthropic.Anthropic()\n\ndef ask(prompt):\n    response = client.messages.create(\n        model=\"claude-sonnet-4-6\",\n        max_tokens=1024,\n        messages=[{\"role\": \"user\", \"content\": prompt}],\n    )\n\n    return response.content[0].text\n\n# Step 1: generate a product name\nname = ask(\"Invent a one-word name for a coffee subscription app.\")\n\n# Gate: bail out if step 1 didn't behave\nif len(name.split()) != 1:\n    raise ValueError(f\"Expected one word, got: {name}\")\n\n# Step 2: use step 1's output\ntagline = ask(f\"Write a short tagline for a coffee app called '{name}'.\")\n\n# Step 3: use step 2's output\npitch = ask(f\"Write a one-sentence elevator pitch using this tagline: '{tagline}'.\")\n```\n\nAbove, we ask an LLM to invent a fictitious name for a coffee subscription app. We tell it to invent a one-word name, but then we check to make sure it does with split(). In the second step, we use that name to generate a short tagline for the app. Finally, a third step uses that tagline to create a one-sentence elevator pitch.\n\nAlternatively, we could pass both the **name** and the **tagline** into step 3:\n\n```\n# Step 3: use both step 1 and step 2's output\n\npitch = ask(\n    f\"Write a one-sentence elevator pitch for a coffee app called '{name}' with the tagline '{tagline}'.\"\n)\n```\n\nStill prompt chaining; we just passed two responses to the next.\n\n# Routing\n\nRouting is a pattern in which we first classify user input and then route it to a specialized follow-up task. We do this 1) to separate concerns, and 2) to build more specialized prompts. Why not just send every request to the same LLM? Typically, when we prompt LLMs via the API, we are concerned with factors such as the token length of our inputs, specialized tools, and whether the model can perform complex reasoning. In scenarios where it is more appropriate to have different LLMs for different tasks, if we fail to route properly, we’ll optimize for one kind of interaction, which can make our other inputs work worse. This scenario works well when you have distinct categories that are better handled separately.\n\nThere are two ways to classify, and you should use this pattern when you have high confidence in the classification step itself:\n\n- Programmatic classification (you classify): Your code uses a set of rules to classify the input. For example, we might scan the input for certain matching keywords.\n\n- LLM Classification. We ask one LLM to decide which other LLM to call (and how).\n\nIn this first example, we have **programmatic routing. **We will look for the words charge, refund, invoice, payment, error, broken, crash, and classify our support requests as billing, technical, or general based on matching keywords in the input.\n\nWith a programmatic router:\n\nWith an LLM Router:\n\n``` python\nimport anthropic\n\nclient = anthropic.Anthropic()\n\ndef ask(prompt, model=\"claude-sonnet-4-6\"):\n    response = client.messages.create(\n        model=model,\n        max_tokens=1024,\n        messages=[{\"role\": \"user\", \"content\": prompt}],\n    )\n    return response.content[0].text\n\ndef classify(query):\n    q = query.lower()\n    if any(word in q for word in [\"charge\", \"refund\", \"invoice\", \"payment\"]):\n        return \"BILLING\"\n    elif any(word in q for word in [\"error\", \"broken\", \"crash\", \"bug\"]):\n        return \"TECHNICAL\"\n    else:\n        return \"GENERAL\"\n\ndef route(query):\n    category = classify(query)\n    if category == \"BILLING\":\n        return ask(f\"You are a billing specialist. Answer: {query}\")\n    elif category == \"TECHNICAL\":\n        return ask(f\"You are a technical support engineer. Answer: {query}\")\n\n    else:\n        return ask(f\"You are a friendly support agent. Answer: {query}\")\nprint(route(\"I was charged twice this month\"))\n```\n\nThe router is classify with the if in route. It’s just scanning for keywords in the input.\n\nTo do this with an LLM router instead, we’ll change the classify function like so:\n\n``` python\ndef classify(query):\n\n    return ask(\n\n        f\"Classify this message into exactly one word: \"\n\n        f\"BILLING, TECHNICAL, or GENERAL.\\n\\nMessage: {query}\"\n\n    ).strip().upper()\n```\n\nPrompt Chaining and Routing both keep the control flow in your code. You decide each next step. The model fills in the part you can’t write by hand. Chaining passes one output into the next call. Routing decides which call to make before anything runs. In both cases, your code is still driving.\n\nIn the next post, I’ll cover Parallelization, Orchestration, and Evaluator-Optimizer.", "url": "https://wpnews.pro/news/ai-introduction-part-3-workflow-patterns-part-1", "canonical_source": "https://jasonfleetwoodboldt.com/2026/08/19/ai-introduction-part-3-workflow-patterns-part-1/", "published_at": "2026-08-19 02:25:30+00:00", "updated_at": "2026-08-19 16:12:09.752195+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-agents", "developer-tools"], "entities": ["Anthropic", "Claude", "ChatGPT"], "alternates": {"html": "https://wpnews.pro/news/ai-introduction-part-3-workflow-patterns-part-1", "markdown": "https://wpnews.pro/news/ai-introduction-part-3-workflow-patterns-part-1.md", "text": "https://wpnews.pro/news/ai-introduction-part-3-workflow-patterns-part-1.txt", "jsonld": "https://wpnews.pro/news/ai-introduction-part-3-workflow-patterns-part-1.jsonld"}}