If you have built a browser agent, you have probably shipped this loop: send the current page to the model, ask what to do next, execute that one action, repeat. It is the default across browser-use, Stagehand's agent mode, and most framework browser tools. The model is the brain, the browser is the body, and every step is a round trip.

There is a different shape. Make one model call that writes the whole plan down as JSON, then run that plan with a deterministic executor that never calls a model at all. This post is what that looks like in practice, including the exact schema we run in production, and honest arithmetic on what it saves.

Why the loop is expensive twice over

The obvious cost is the round trips. The less obvious one is what you send on each of them.

An agent loop has to include the current page state on every call, because that is the only thing telling the model where it is. A serialised DOM or accessibility tree for a real signup page is commonly ten to fifty thousand tokens. Twenty steps at twenty five thousand tokens each is half a million input tokens for one task.

The structural cost is worse. At every step the model has to re-derive the plan it already held implicitly one step earlier. Ask it to sign up for a product and it sketches the same sequence in its head each time: go to the signup page, fill the email, submit, wait for the code, paste the code, verify. Then it picks one action and throws the rest away. Next step, it sketches it again. Most of the time it lands in the same place. Sometimes it picks the login link instead of the signup link, and every subsequent step compounds that.

Linear goals do not need this. "Sign up with this address" has one obvious sequence. The model is very good at writing that sequence down once. It is mediocre at re-deriving it twenty times without drifting.

The shape

goal --> planner (1 LLM call) --> JSON plan --> executor (0 LLM calls) --> result

The planner gets one shot and is told so in its system prompt. It has no follow-up call to correct itself, which is deliberate: it makes the model plan defensively rather than optimistically.

The ten step types, exactly

This is the real schema, not a simplification. A plan is { "reasoning": string, "steps": Step[] } and a step is one of:

{ "type": "navigate", "url": string }
{ "type": "click", "describe": string, "text"?: string, "selector"?: string }
{ "type": "fill", "describe": string, "selector"?: string, "label"?: string, "value": string }
{ "type": "wait_seconds", "seconds": number }
{ "type": "wait_for_text", "text": string, "timeout_seconds"?: number }
{ "type": "wait_for_email", "inbox_id"?: string, "from"?: string, "subject"?: string, "timeout_seconds"?: number }
{ "type": "use_otp_from_inbox", "inbox_id"?: string, "selector": string, "timeout_seconds"?: number }
{ "type": "open_link_from_inbox", "inbox_id"?: string, "from"?: string, "timeout_seconds"?: number }
{ "type": "extract_text", "describe": string, "selector"?: string }
{ "type": "done", "summary": string }

Three constraints in the planner prompt do most of the quality work:

  • Every plan ends with done. The executor breaks out of its loop on done and takes the summary as the task result. A plan without one runs to the end of the array and returns a generic summary.
  • Prefer text over selector on a click. Humans see text, and text survives a CSS refactor. Selectors are the fallback, not the default.
  • Keep plans under fifteen steps. Long flows fail. If the goal is bigger than that, the planner is told to plan the first sub-goal only and emit done with a summary of what is left, so the caller can decide whether to continue.

Values can carry {{inbox_address}}, which the executor substitutes before each step runs. That is what lets a plan be written before the inbox it will use exists.

The three steps that cross into email

Six of the ten types are ordinary browser actions. Three are not, and they are the reason this architecture is worth describing at all:

  • wait_for_email blocks until a message matching the filter lands in the bound inbox
  • use_otp_from_inbox waits for the mail, extracts the code, and fills it into a selector
  • open_link_from_inbox waits for the mail, finds the verification or magic link, and navigates to it

In an agent loop, "check the email" is not an action the browser can take. You break out of the loop, poll a mail API in your own code, feed the result back in as text, and hope the model picks up where it left off. That handoff is where a lot of signup automation actually breaks.

Here it is one step in the plan, because the inbox and the browser session are in the same runtime. A full email-verified signup:

{
  "reasoning": "Standard email signup with a code sent to the inbox.",
  "steps": [
    { "type": "navigate", "url": "https://example.com/signup" },
    { "type": "fill", "describe": "email field", "label": "Email", "value": "{{inbox_address}}" },
    { "type": "click", "describe": "submit the signup form", "text": "Sign up" },
    { "type": "wait_for_email", "from": "example.com", "timeout_seconds": 120 },
    { "type": "use_otp_from_inbox", "selector": "input[name='code']" },
    { "type": "click", "describe": "confirm the code", "text": "Verify" },
    { "type": "wait_for_text", "text": "Welcome", "timeout_seconds": 30 },
    { "type": "done", "summary": "Account created and email verified." }
  ]
}

No glue code between the browser and the mailbox, because there is no boundary to glue across.

The arithmetic

We have not run a published benchmark, so here is the calculation instead of a number to take on faith. Substitute your own figures; the shape holds.

Take a twenty step task, twenty five thousand tokens of page state per step, priced at Claude Sonnet rates of $2 per million input tokens and $10 per million output.

Agent loop. Twenty calls carrying 25k tokens each is 500,000 input tokens, or $1.00, before a single output token. Add roughly 500 output tokens per step and you are at about $1.10.

Plan then execute. One call. The system prompt is roughly 600 tokens and is marked for caching, so it costs about a tenth of that on every call after the first. The goal plus inbox context is perhaps 1,000 tokens. Output is capped at 2,048. Call it 1,700 input and 800 output: about $0.011.

That is roughly two orders of magnitude, and the dominant term is not the number of calls. It is that the loop pays for the page state twenty times and the planner never pays for it at all. If your pages serialise smaller, the gap narrows proportionally. If you run more steps, it widens.

One thing that is easy to get wrong here: the planner call is a good candidate for prompt caching precisely because the system prompt is frozen and comes first, while the volatile part (the goal, the inbox context) goes last. Reverse that order and the cache never hits.

What this architecture does not do

Being straight about the limits, because they are the reason to keep a loop around for some jobs.

There is no replanning. The executor runs each step, and on the first failure it records the error, stops, and returns everything up to that point. It does not retry, ask a model for a fix, or improvise. When a page has changed since the plan was written, the plan is wrong and the task fails. That is a deliberate trade: predictable failure with a full step-by-step event log beats an agent that quietly does the wrong thing for another fifteen steps.

There is no plan cache. Every call plans from scratch today. Caching by goal and domain is the obvious next win and it is not built.

Exploratory goals do not fit. "Find a product under fifty dollars" or "research this site and report back" have no linear sequence to write down. The whole premise is that the plan is knowable in advance. When it is not, you want the loop, and you should pay for it.

The fifteen step cap is real. Longer plans get noisier and the model starts over-explaining. Splitting into sub-goals with an explicit handoff summary works better than raising the cap.

When to use which

Goal shapeUse
Linear and repeatable: signups, email verification, password resets, form fills, single page extractionPlan then execute
Branchy and exploratory: research, comparison, navigating an unfamiliar menu treeStep level agent loop

Most production browser automation is the first row. It is worth knowing that the first row does not need an agent at all.

The general version

This is not really about browsers. Whenever a model's strength is sketching a sequence once and its weakness is re-deriving that sequence repeatedly, plan then execute wins: multi step API workflows, file operations, migration scripts. The test is simple. Ask whether the plan is knowable before you start. If it is, write it down once and run it with an interpreter you control. If it genuinely is not, that is what the loop is for.

The runtime described here is the browser act endpoint: one call, one planner hit, deterministic execution, with the inbox in the same runtime so the verification steps are primitives rather than glue.