Agentic AI

Workflows And Multi Agent Systems

Workflow Patterns

Every pattern in this chapter is a workflow by Module 1, Chapter 2's test: the developer decided the control flow, not the model.

JrCodex·7 min read

Jr Codex Agentic AI Notes

Level: Advanced Prerequisites: Module 5, Chapter 3: LangGraph Time to complete: ~25 minutes


Table of Contents

  1. Orchestration Without Autonomy
  2. Sequential Chaining
  3. Parallel Fan-Out
  4. Routing
  5. Evaluator-Optimiser
  6. Event-Driven Workflows
  7. Composing Patterns
  8. Summary & Next Steps

1. Orchestration Without Autonomy

Every pattern in this chapter is a workflow by Module 1, Chapter 2's test: the developer decided the control flow, not the model.

Why a Module on Agents Covers Workflows
─────────────────────────────────────────
  Because most problems people solve with agents are
  workflows, and the workflow version is cheaper,
  faster, testable and reproducible.

  These patterns also compose WITH agents — an agent
  inside a workflow node is common and good. What is
  usually wrong is an agent where a workflow would
  do.
─────────────────────────────────────────

2. Sequential Chaining

When It Applies
─────────────────────────────────────────
  A ──► B ──► C

  Each stage's output is the next stage's input, and
  the order is known.

  Use when: the task genuinely decomposes into fixed
  stages — extract, then transform, then format.
─────────────────────────────────────────
def sequential(client, document):
    facts   = extract(client, document)              # each stage gets a CLEAN context
    checked = verify(client, facts, document)        # containing only what it needs
    return summarise(client, checked)
The Underrated Benefit
─────────────────────────────────────────
  Each stage has its own small context, so none of
  them suffers the degradation from Module 2,
  Chapter 1.

  Three focused calls frequently beat one long agent
  run on quality, not just on cost — because each
  call is doing one thing with nothing else in the
  way.
─────────────────────────────────────────
The Gate Variant
─────────────────────────────────────────
  Insert a cheap check between stages and stop early
  when it fails:

    extract ──► [is it a valid invoice?] ──► no ──► STOP
                        │ yes
                        ▼
                     transform

  Saves the expensive stages on inputs that were
  never going to work.
─────────────────────────────────────────

3. Parallel Fan-Out

import asyncio
 
async def fan_out(client, document, aspects):
    """Independent analyses, concurrently, then combined."""
    results = await asyncio.gather(*[
        analyse(client, document, aspect) for aspect in aspects
    ])
    return await synthesise(client, dict(zip(aspects, results)))
 
# Latency becomes that of the SLOWEST branch, not the sum.
report = await fan_out(client, contract,
                       ["payment terms", "liability", "termination", "IP"])
Two Distinct Uses
─────────────────────────────────────────
  SECTIONING     different branches do DIFFERENT
                 work on the same input (the example
                 above). Combine by concatenation or
                 synthesis.

  VOTING         several branches do the SAME work,
                 and you take the majority. This is
                 the Gen AI Notes' self-consistency,
                 applied to a workflow stage.
─────────────────────────────────────────
async def vote(client, question, n=3):
    answers = await asyncio.gather(*[classify(client, question) for _ in range(n)])
    counts = collections.Counter(answers)
    answer, votes = counts.most_common(1)[0]
    return answer, votes / n            # the AGREEMENT RATE gates what happens next

4. Routing

from pydantic import BaseModel
from typing import Literal
 
class Route(BaseModel):
    category: Literal["billing", "technical", "refund", "other"]
    confidence: Literal["high", "low"]
 
def route(client, ticket):
    r = client.chat.completions.parse(
        model="gpt-4o-mini",                          # a SMALL model is enough to route
        messages=[{"role": "system", "content": ROUTING_RULES},
                  {"role": "user", "content": ticket}],
        response_format=Route, temperature=0,
    ).choices[0].message.parsed
 
    if r.confidence == "low":
        return handle_human_review(ticket)            # do not guess on a weak signal
    return HANDLERS[r.category](client, ticket)
Why Routing Is Usually Better Than an Agent
─────────────────────────────────────────
  A router plus specialised handlers gives each
  branch a FOCUSED prompt and a SMALL tool set —
  three tools rather than fifteen.

  Module 2, Chapter 3 noted that tool selection
  accuracy degrades past ~20 tools. Routing is the
  cheapest way to keep every branch well under that
  threshold.

  You also get to use a cheap model for the routing
  decision and an expensive one only where it
  matters.
─────────────────────────────────────────

5. Evaluator-Optimiser

A generator and a separate evaluator, looping — Module 4, Chapter 3's reflection as an explicit workflow.

def evaluator_optimiser(client, task, criteria, max_rounds=3):
    output = generate(client, task)
 
    for round_num in range(max_rounds):
        verdict = evaluate(client, output, criteria)      # a SEPARATE call, fresh context
        if verdict.passed:
            return output, round_num
        output = revise(client, task, output, verdict.issues)
 
    return output, max_rounds                             # return best effort, flagged
When This Shape Earns Its Cost
─────────────────────────────────────────
  Use it when the criteria are EXPLICIT and the
  evaluation is cheaper than the generation —
  Module 4, Chapter 3's condition.

  Best case: the evaluator is not an LLM at all but
  a compiler, a test suite or a schema validator.
  Then the loop is nearly free and the signal is
  perfect.
─────────────────────────────────────────

6. Event-Driven Workflows

Everything above is request-response. Real systems often need work triggered by events, running in the background.

async def on_event(event, ctx):
    """One handler, dispatched by event type. Each run is short and bounded."""
    match event.type:
        case "ticket.created":
            route_and_enqueue(event.payload)
        case "investigation.completed":
            if event.payload["severity"] == "high":
                await ctx.emit("escalation.required", event.payload)   # CHAIN by event
        case "approval.granted":
            await ctx.resume(event.payload["thread_id"])               # Chapter 2
Why Event-Driven Suits Agents
─────────────────────────────────────────
  LONG RUNS       an agent taking four minutes does
                  not hold an HTTP connection (Gen AI
                  Notes, Module 7, Chapter 1)

  HUMAN GATES     "wait for approval" is naturally an
                  event, not a blocking call

  RETRY           a failed handler re-runs from the
                  queue without re-running everything
                  before it

  BACKPRESSURE    the queue absorbs spikes instead of
                  hammering a rate-limited model API
─────────────────────────────────────────
The Requirement It Imposes
─────────────────────────────────────────
  Handlers must be IDEMPOTENT. Queues deliver at
  least once, so every handler will eventually run
  twice on the same event.

  Same discipline as Module 2, Chapter 3's tool
  design: an idempotency key, checked before acting.
─────────────────────────────────────────

7. Composing Patterns

Real systems are combinations. The composition is where the design lives.

A Support System, Assembled
─────────────────────────────────────────
  ticket ──► ROUTE (cheap model)
               │
    ┌──────────┼──────────┐
    ▼          ▼          ▼
  billing   technical   refund
  (chain)   (AGENT)     (chain + human gate)
    │          │          │
    └──────────┼──────────┘
               ▼
        EVALUATE the draft reply
               │
       ┌───────┴────────┐
     pass             fail ──► revise (max 2)
       ▼
      send
─────────────────────────────────────────
Read the Design Choices
─────────────────────────────────────────
  ROUTING first, so each branch is focused and cheap.

  Only the TECHNICAL branch is an agent — it is the
  one whose steps genuinely depend on what is found.
  Billing and refunds are known procedures.

  The REFUND branch has a human gate because it
  moves money (Module 8, Chapter 2).

  ONE evaluator at the end, shared by all branches —
  quality control in one place rather than three.
─────────────────────────────────────────
The Principle
─────────────────────────────────────────
  Push autonomy DOWN and IN.

  Keep the outer structure deterministic and
  testable. Give agent autonomy only to the specific
  inner nodes that need it.

  This is how you get an agent's flexibility with a
  workflow's predictability, and it is the shape
  most successful production systems converge on.
─────────────────────────────────────────

8. Summary & Next Steps

Key Takeaways

  • These patterns are workflows: the developer decides control flow, which makes them cheaper, testable and reproducible — and most agent problems are really workflow problems.
  • Sequential stages give each call a clean, small context, so three focused calls often beat one long agent run on quality as well as cost.
  • Routing keeps each branch's tool set small and lets a cheap model make the routing decision, directly addressing tool-selection degradation.
  • Compose by pushing autonomy down and in: a deterministic outer structure with agent autonomy only at the nodes that genuinely need it.

Concept Check

  1. Why can splitting one agent run into three sequential calls improve quality rather than just cost?
  2. What property must event handlers have, and which earlier chapter's discipline does it reuse?
  3. In the composed support system, why is only the technical branch an agent?

Next Chapter

Chapter 2: State, Checkpointing & Durability


Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to Agentic AI Index