cd /news/large-language-models/when-a-pipeline-beats-an-agent-three… · home topics large-language-models article
[ARTICLE · art-107243] src=dev.to ↗ pub= topic=large-language-models verified=true sentiment=· neutral

When a pipeline beats an agent: three shapes that don't need a loop

A developer's field notes argue that many LLM tasks are over-engineered as agents, and instead propose three fixed pipeline shapes—sequential steps, single classification, and parallel fan-out—that avoid the quadratic token cost and failure surface of model-controlled loops. The post includes code examples showing how to implement these patterns with multiple model calls but no runtime branching, and advises handling rare deviations as explicit branches rather than turning the whole pipeline into a loop.

read5 min views3 publishedAug 22, 2026

Originally published on Loop & Retry — field notes on building LLM agents that survive production.

When not to build an agent made the case in the abstract: an agent is an LLM that controls its own control flow, and that control costs you quadratic tokens, serial latency, and a failure surface no unit test can cover, on every single run. What that post didn't give you is the thing you reach for instead. This is that post — three pipeline shapes, each a fixed sequence of calls with no model-decided branching, that between them cover most of the tasks I've seen get a loop by default.

The shared property across all three: you can draw the flowchart of every possible run before you execute a single one. That's the actual dividing line, not "does it call an LLM more than once" — all three shapes below call a model multiple times. What they don't do is let the model decide, at runtime, what step comes next.

The simplest shape and the most commonly reached-for-a-loop task: a fixed sequence of steps, each feeding the next, where the order is known in advance even though the content isn't. Extract, then validate, then format is the canonical example.

def process_ticket(raw_text: str) -> dict:
    extracted = client.messages.create(
        model="claude-sonnet-4-5", max_tokens=512,
        messages=[{"role": "user", "content": f"Extract fields as JSON: {raw_text}"}],
    )
    fields = json.loads(extracted.content[0].text)

    validated = client.messages.create(
        model="claude-sonnet-4-5", max_tokens=256,
        messages=[{"role": "user", "content": f"List any missing/invalid fields: {fields}"}],
    )
    issues = json.loads(validated.content[0].text)

    if issues:
        return {"status": "needs_review", "fields": fields, "issues": issues}

    formatted = client.messages.create(
        model="claude-sonnet-4-5", max_tokens=256,
        messages=[{"role": "user", "content": f"Format for the ticketing API: {fields}"}],
    )
    return {"status": "ok", "payload": formatted.content[0].text}

Three model calls, zero loops. Nothing here decides "what to do next" at runtime beyond a single if issues

branch, and that branch has exactly two known destinations. Compare this to an agentic version of the same task — a loop where the model decides after each step whether to extract again, validate again, or call a different tool — and you're paying for a decision that, in the actual failure data, almost always resolves to "proceed to the next fixed step anyway." You're running an agent to reimplement a straight line.

The tell that you've over-built this into an agent: if you trace real runs and the "decide what's next" step picks the same next step upward of, say, 95% of the time, you've built a loop around a straight line and paid the loop's tax for the 5% case. Handle that 5% as an explicit branch (like issues

above), not as license for the whole pipeline to become a loop.

The task genuinely needs a decision — but the decision is a single classification, not an open-ended sequence. A support ticket needs to go to one of five fixed playbooks; a document needs one of three fixed extraction templates. Bound the model's discretion to exactly one classification call, then hand off to ordinary code:

HANDLERS = {
    "billing": handle_billing_ticket,
    "bug_report": handle_bug_ticket,
    "access_request": handle_access_ticket,
    "feature_request": handle_feature_ticket,
    "other": handle_general_ticket,
}

def route_ticket(raw_text: str) -> dict:
    classification = client.messages.create(
        model="claude-haiku-4-5", max_tokens=32,
        messages=[{"role": "user", "content":
            f"Classify into exactly one of {list(HANDLERS)}: {raw_text}"}],
    ).content[0].text.strip()

    handler = HANDLERS.get(classification, handle_general_ticket)
    return handler(raw_text)  # each handler is its own fixed chain (Shape 1)

This is the shape people mean when they say "the agent decides what to do" — and it's true, narrowly. It decides once, among a known, enumerable set of outcomes, and every outcome routes to code you wrote and can test independently of the model. Compare that to a real agent loop, where the model can in principle keep re-deciding after every step, with a branching factor that compounds with every turn. A five-way classification with five fixed downstream chains is testable exhaustively — five cases, five expected handlers. A loop with five tools available at every one of ten steps has, in the worst case, five-to-the-tenth possible trajectories, and you will test approximately none of them.

Independent sub-tasks over a known list, with no dependency between them, then a fixed combine step. Summarizing forty documents and writing one digest is the standard example — each summary doesn't need to know about the others, and the number of summaries is known before you start.

async def digest_documents(docs: list[str]) -> str:
    summaries = await asyncio.gather(*[
        summarize_one(doc) for doc in docs  # N independent calls, fixed N, no loop
    ])
    return client.messages.create(
        model="claude-sonnet-4-5", max_tokens=1024,
        messages=[{"role": "user", "content":
            f"Combine these {len(summaries)} summaries into one digest:\n" + "\n---\n".join(summaries)}],
    ).content[0].text

This is the shape most likely to get mislabeled as needing an agent purely because it involves "a lot of LLM calls." It doesn't need control flow — it needs concurrency, which is a much cheaper problem. The fan-out calls parallelize (wall-clock cost is one call deep, not forty calls deep), each one fails and retries independently without touching the others, and the reduce step is a single deterministic hand-off. None of that requires anything to decide what happens next at runtime.

All three shapes share the same limit: they work exactly as long as the sequence of steps is knowable ahead of time, even when the content of each step isn't. The real test for whether a task needs a loop is whether the branching factor is knowable in advance — not whether the task is "complex," not whether it calls a model more than once, but whether you can enumerate the graph of possible next-steps before you've seen this run's intermediate results.

A debugging agent that has to decide, based on what a failing test actually says, whether to read a file, run a different test, or grep the codebase — and where that decision genuinely depends on content you can't predict, and can recur an unknown number of times — is a real case for a loop. You can't pre-draw that flowchart, because the number of nodes in it depends on what today's failure looks like. That's the difference between "the model picks one of five known destinations once" (Shape 2) and "the model picks the next of an unknown number of destinations, repeatedly, based on results it hasn't seen yet" (an actual agent) — and it's worth writing down the expected graph for your task before you build either one, because most tasks that people bring a loop to turn out, on inspection, to have already fully enumerable graphs.

── more in #large-language-models 4 stories · sorted by recency
── more on @loop & retry 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/when-a-pipeline-beat…] indexed:0 read:5min 2026-08-22 ·