Loops and beads: orchestrating AI agents with Postman

Loops and beads: orchestrating AI agents with Postman

Quinton Wall

Every AI agent boils down to the same three moves: decide what to do, do it, look at the result. The part that matters is how you wire those moves together. Get the wiring wrong and a two-second task takes six. Get it wrong badly enough and a single stuck step takes the whole agent down with it.

I’ve been thinking about this in terms of two patterns I wrote about separately: loops and beads. A loop is temporal repetition: call the model, run a tool, feed the result back, repeat until done. A bead is structural composition: a small unit of work with a defined input and output, wired into a graph alongside other beads. Loops are great for open-ended, conversational tasks. Beads are better once you know the shape of the work in advance and some of it can happen in parallel.

The Postman plugin for Claude Code turned out to be a good place to see the difference firsthand. It ships eight commands, backed by the Postman MCP server, for syncing collections, generating client code, running tests, creating mocks, publishing docs, auditing security, and scoring API readiness. In this post I’ll build the same small agent twice against the Postman API, once as a loop and once as a bead graph, and show you the code so you can run both yourself.

What loops and beads mean in code

A loop agent is one conversation with the model that keeps going until the model says it’s done. You define a set of tools, hand the model a question, and every time it asks for a tool call, you run that tool and feed the result back in. Claude’s Messages API is stateless, so each turn resends the full conversation history, and the model decides one step at a time what happens next. That’s the ReAct pattern most agent frameworks default to, and it’s a fine default: it’s simple, and the model can change its mind mid-task.

A bead agent is a graph you define up front. Each bead is a function with a name, a job, and a list of dependencies. A bead only runs once its dependencies have finished, and beads with no dependency on each other run at the same time. Nothing here is unique to Postman or Claude. It’s closer to how you’d design a build pipeline or a data processing DAG than to a chat loop, and that’s exactly the point: once a task’s structure is known, you don’t need the model to rediscover it turn by turn.

The plugin’s own command set is already bead-shaped. /postman:test and /postman:security don’t depend on each other’s output. Neither does /postman:docs. If an agent needs all three, running them one after another in a loop wastes time waiting on network calls that have nothing to do with each other. That’s the exact case beads are built for.

Building the same agent twice

To make the comparison concrete, I wrote one task two ways: “Is my API collection healthy?” The task needs two pieces of information, a summary of a Postman Collection and the pass/fail result of a Postman Monitor run. Neither depends on the other.

The full, runnable script is in postman_loops_and_beads.py in the companion repo. I’ll walk through the parts that matter here, but grab the file if you want to run it against your own workspace.

The loop version

The loop agent gives Claude two tools and lets it decide when to call them:

TOOLS = [
    {
        "name": "get_collection_summary",
        "description": "Fetch a Postman Collection by ID and summarize its request count and auth type.",
        "input_schema": {
            "type": "object",
            "properties": {"collection_id": {"type": "string"}},
            "required": ["collection_id"],
        },
    },
    {
        "name": "run_monitor",
        "description": "Trigger a Postman Monitor run by ID and return its pass/fail summary.",
        "input_schema": {
            "type": "object",
            "properties": {"monitor_id": {"type": "string"}},
            "required": ["monitor_id"],
        },
    },
]

The loop itself is short. Call the model, check if it asked for a tool, run the tool, append the result, repeat:

while True:
    response = await anthropic.messages.create(
        model=MODEL, max_tokens=1024, tools=TOOLS, messages=messages
    )
    messages.append({"role": "assistant", "content": response.content})

    if response.stop_reason != "tool_use":
        break

    tool_results = []
    for block in response.content:
        if block.type == "tool_use":
            result = await call_tool(block.name, block.input)
            tool_results.append(
                {"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(result)}
            )
    messages.append({"role": "user", "content": tool_results})

This works and it’s easy to follow. The catch is timing. Claude can only ask for one thing at a time in this setup, so it fetches the collection, waits for the result, then decides to run the monitor, and waits again. The two lookups never overlap even though nothing requires them not to.

The bead version

The bead version replaces the loop with an explicit graph. A bead is a small dataclass:

@dataclass
class Bead:
    name: str
    run: Callable[[dict, dict], Awaitable[Any]]
    deps: tuple[str, ...] = ()

And a graph runner that runs every bead whose dependencies are already done, one layer at a time, using asyncio.gather to run each layer concurrently:

async def run_graph(beads: list[Bead], context: dict) -> dict[str, Any]:
    done: dict[str, Any] = {}
    remaining = {b.name: b for b in beads}

    while remaining:
        ready = [b for b in remaining.values() if all(d in done for d in b.deps)]
        if not ready:
            raise RuntimeError(f"Unmet bead dependencies: {list(remaining)}")

        results = await asyncio.gather(*(b.run(context, done) for b in ready))
        for bead, result in zip(ready, results):
            done[bead.name] = result
            del remaining[bead.name]

    return done

The graph for this agent has four beads. classify_intent runs first, using a fast Haiku call to decide whether the question needs the collection lookup, the monitor run, or both, so a bead can skip its own work based on what an earlier bead decided. That’s the first-class branching the beads post talks about: the decision lives in the graph, not buried in a prompt. fetch_collection and run_monitor both depend only on classify_intent, so they run at the same time. build_report depends on both and merges their output into an answer:

beads = [
    Bead("classify_intent", bead_classify_intent),
    Bead("fetch_collection", bead_fetch_collection, deps=("classify_intent",)),
    Bead("run_monitor", bead_run_monitor, deps=("classify_intent",)),
    Bead("build_report", bead_build_report, deps=("fetch_collection", "run_monitor")),
]
done = await run_graph(beads, context)

Four beads, three layers, and the middle layer runs both of its beads in parallel instead of waiting on itself.

Running the comparison

The script has a --demo flag that swaps the real Postman API calls for canned responses with realistic delay (1.2 seconds for the collection lookup, 1.8 seconds for the monitor run), so you can see the timing difference without a Postman API key:

pip install anthropic httpx python-dotenv
echo "ANTHROPIC_API_KEY=sk-ant-..." > .env

python3 postman_loops_and_beads.py --demo --pattern compare

On my machine that prints something close to this:

=== loop pattern ===
Your collection has 14 requests using bearer auth, and the monitor run passed all 22 assertions.

[loop] wall clock: 3.02s

=== beads pattern ===
Your collection (14 requests, bearer auth) looks fine, and the monitor run passed all 22 assertions with no failures.

[beads] wall clock: 1.83s

beads finished faster (loop: 3.02s, beads: 1.83s) because independent beads ran in parallel.

The loop’s time is close to the sum of both simulated delays, 1.2 seconds plus 1.8 seconds. The bead graph’s time is close to the slower of the two, because fetch_collection and run_monitor ran together. That’s the same sum-or-slowest-operation tradeoff from the beads post‘s travel-brief example, measured here instead of described.

Drop --demo and add real IDs to hit the live Postman API with your own credentials. Add your Postman API key to the same .env file:

echo "POSTMAN_API_KEY=PMAK-..." >> .env

python3 postman_loops_and_beads.py --pattern beads \
  --collection <your_collection_id> --monitor <your_monitor_id>

Where each pattern fits with the plugin

I wouldn’t rewrite every agent as a bead graph. The two patterns solve different problems, and the plugin’s own commands split cleanly along that line.

A loop still makes sense for anything genuinely conversational, where you don’t know the next step until you see the result of the last one. Debugging a failing test with /postman:test, following up on findings from a /postman:security audit, or iterating on client code with /postman:sync all fit that shape. The model needs room to change direction.

A bead graph pays off once you know the shape of the work ahead of time and some of it doesn’t depend on the rest. /postman:test and /postman:security don’t read each other’s output. Neither does checking a spec against the agent readiness analyzer that scores APIs across eight pillars. An agent that runs “test, audit, and score” as three independent beads finishes in roughly the time of the slowest one, not all three added together. And if the security audit bead fails, you retry that bead alone instead of rerunning tests that already passed. That’s the granular re-entry the beads post calls out, and it matters more as the number of steps grows.

Try it yourself

Clone the script and run it against a real Postman workspace:

git clone https://github.com/quintonwall/loops-and-beads.git
cd loops-and-beads
pip install anthropic httpx python-dotenv
cp .env.example .env   # then fill in your own keys

Then try changing the question. Ask it to only check the monitor, and watch classify_intent skip the collection lookup entirely. Add a third bead, maybe a call to the agent readiness checks, depending only on classify_intent, and it joins the parallel layer for free. That’s the part of beads that’s easy to miss from the outside: adding independent work doesn’t add latency. It adds another item to the same asyncio.gather call.

If you haven’t set up the Postman plugin for Claude Code yet, install it and run /postman:setup to authenticate. Once it’s running, try asking it to test and audit an API in the same message and watch which commands it fires off. You’ll start noticing which parts of your own agent workflows are secretly loops and which ones have been beads all along.

Resources

What do you think about this topic? Tell us in a comment below.

Comment

Your email address will not be published. Required fields are marked *


This site uses Akismet to reduce spam. Learn how your comment data is processed.