I run a production system that is almost entirely automated. It spawns subagents, calls models hundreds of times a day, and ships real work. The surprising part: most of it is not agents. It is plain code calling single LLM calls at the right moments. The agent loops are a small, deliberate minority, and every one of them earns its place. Here is how I decide.

Decision tree: single LLM call vs prompt chain vs fixed workflow vs AI agent

What actually separates a chain from an agent?

A chain is a function you wrote; an agent is a loop the model drives. That's the whole distinction, and it's the one people skip.

In a prompt chain, your code owns the control flow. You decide the order, the branches, and where each model call goes. An agent hands that control flow to the model. The LLM decides what to do next, does it, looks at the result, and decides again until it thinks it's done. That autonomy is powerful and expensive. You trade determinism for the ability to handle problems whose shape you do not know ahead of time. The simplest chain of all is a single call, so start there and climb only when you must.

from anthropic import Anthropic
      client = Anthropic()

      # Single call: you own the control flow.
      def classify(ticket: str) -> str:
          msg = client.messages.create(
              model="claude-sonnet-5",
              max_tokens=20,
              messages=[{"role": "user",
                         "content": f"Classify this ticket as bug/billing/other:\n{ticket}"}],
          )
          return msg.content[0].text.strip()
      

That function is boring, and boring is the point. It runs in one round trip, costs one set of tokens, and returns the same shape every time. You can unit-test it, cache it, and reason about it.

When does a single call win? (default here)

A single call wins whenever the task is one transformation and the context fits in the prompt. Classification, extraction, summarization, rewriting, translation, structured-data generation, sentiment, routing: these are single calls, and dressing them up as agents only adds failure modes.

Three reasons the single call is the default:

  • Cost. An agent that takes five internal steps sends roughly five times the tokens, and every step re-sends the growing history. A ten-step agent can cost twenty times a single call for the same answer. Prompt caching claws some of that back, since cache reads run about ten times cheaper, but you still pay for every fresh loop.
  • Latency. Each loop iteration is a full round trip. One call is one round trip. Users feel the difference between 300ms and 15 seconds.
  • Debuggability. When a single call is wrong, you read one prompt and one response. When an agent is wrong, you replay a branching transcript and guess which of nine decisions went sideways.

If you can name the exact steps in advance, you don't need an agent. You need code.

When does an agent earn its complexity?

An agent earns its keep when the number of steps is unknown, the path branches on results you cannot predict, and the model needs tools to act on the world. Think "investigate this failing test until you find the cause," not "summarize this text."

The tell is uncertainty about the path. A coding agent does not know how many files it must read before it finds the bug. A research agent does not know which search will surface the answer. That's real agent territory, because a fixed script cannot encode a path that depends on what the model learns mid-task.

# Agent: the MODEL owns the control flow. Note the while loop.
      def agent(goal: str, tools: dict) -> str:
          messages = [{"role": "user", "content": goal}]
          for _ in range(10):  # a hard cap is not optional
              resp = client.messages.create(
                  model="claude-sonnet-5", max_tokens=1024,
                  tools=[t["schema"] for t in tools.values()],
                  messages=messages,
              )
              if resp.stop_reason != "tool_use":
                  return resp.content[-1].text  # text is the last block when thinking is off
              # Claude can return several tool_use blocks at once (parallel tools),
              # so collect every result and append ONE user turn. Splitting them into
              # separate messages is the bug everyone ships first.
              messages.append({"role": "assistant", "content": resp.content})
              results = []
              for block in resp.content:
                  if block.type == "tool_use":
                      output = tools[block.name]["run"](**block.input)
                      results.append({"type": "tool_result",
                                      "tool_use_id": block.id, "content": str(output)})
              messages.append({"role": "user", "content": results})
          return "hit step limit"
      

Look at what the loop buys you and what it costs. It buys adaptation: the model can read a file, decide it needs another, and keep going. It costs you a hard step cap, a growing context window, tool-error handling, and a transcript you have to trust. You take that trade only when the adaptation is the whole point.

The prompt chain most people mislabel as an agent

Between "one call" and "full agent" sits the option that solves 80% of the hard cases: a prompt chain where your code orchestrates several calls. You keep the control flow. The model just fills in the smart parts.

# Fixed workflow: known steps, deterministic order, no autonomy.
      def triage(ticket: str) -> dict:
          category = classify(ticket)                      # call 1
          if category == "billing":
              summary = summarize_for(ticket, team="finance")   # call 2a
          else:
              summary = summarize_for(ticket, team="eng")       # call 2b
          return {"category": category, "summary": summary}
      

This is a prompt chain, and it's not an agent. You know there are exactly two calls. You know the order. The branch is a plain if, not a model decision. You get the intelligence of the model with the reliability of code, and you can test every path. When people say "we built an agent" and it works great in production, this is usually what they actually built.

The decision, in one pass

Walk the tree in the diagram top to bottom and stop at the first match:

  1. One well-defined transformation, context in hand? Single call. Do not overthink it.
  2. Multiple steps, no tools, order known? Prompt chain. Code the steps.
  3. Steps known, tools involved, path fixed? Fixed workflow. Code owns the flow.
  4. Path genuinely unknown until the model runs? Agent. Cap the loop and watch it.

The bias should point up the list, not down. Every rung down adds cost, latency, and surface area for bugs. Start at the top and only descend when the task forces you to.

The takeaway

Agents are not an upgrade to a prompt chain. They are a different tool with a different price tag. In 2026 the fastest way to a slow, expensive, flaky feature is to reach for an agent when a chain would do. Ask one question before you build: do I know the steps in advance? If yes, you want a chain, not autonomy. If no, and only if no, you want an agent, and you want a step limit on it.

The best "agentic" systems I have shipped are mostly not agents. They are boring chains with a few smart calls in the right places, and one or two real loops where the path is truly unknown. Boring scales. Autonomy is the exception you spend deliberately.


I write these from real work at astraedus.dev, where I build apps and tools. Building something, or stuck on something like this? Reach me at astraedus.dev or [email protected].

Get the next one in your inbox → subscribe at astraedus.dev.