Stagehand v4: What Actually Happens When You Run It We installed Stagehand 4.0.0, ran it against a real page, measured every token it spent, and broke it on purpose. This picks up exactly where our leaves off: no more "what is it," only "what happens when you type and start calling it." Every number below comes from a script you can find and rerun, not from the docs. How we verified this. Stagehand 4.0.0, Ubuntu 24.04, Node 22, Chrome 151, tested locally and on Browserbase, against 4geeks.com and other live pages, in August 2026. Where a finding contradicts our own earlier notes from this same investigation, we say so. Install: faster than you'd expect, with one sharp edge pulls 51 packages in about 4.5 seconds and downloads no browser at all . If you're coming from Playwright, that's the first surprise: v4 drives the Chrome you already have, through , not a browser it manages for you. The one thing that will stop a reader cold: writes into . corepack refuses that range outright: corepack wants an exact version, and doesn't give it one. sidesteps the whole thing. Two more setup traps worth knowing before you touch code: Stagehand does not read your environment variables , despite a provider table in the docs with an "Environment Variable" column that reads like auto detection. Set and call it a day, and you get , an error that points at authentication when the real problem is that nothing ever read the variable. Browserbase signup requires phone number verification . Not a card, but a real barrier worth knowing about before a tutorial sends someone there mid lesson. The docs will send you to the wrong page first Search for Stagehand's model configuration and the top result is , which serves the v3 page: 13 providers listed. The current page, at , lists 5. A reader who follows the obvious link gets a provider list that no longer exists. That same stale surface is where the most common first error comes from. The docs' own Model Gateway example writes: In v4, a bare string for throws a : . The real shape is: That snippet isn't wrong, exactly. It's valid Python (the Python SDK really does accept a string, with the key as a separate argument), sitting on a TypeScript doc page. has the same problem, and it's worse because of how the error reads. Every example in circulation, including the official docs, writes: The thrown error names : . That reads like your instruction is malformed. It isn't. The real signature is positional: With no schema at all, returns , a plain string with no structure. Worth knowing before you assume it failed silently. A few smaller Playwright habits that break the same way: is async now (ported code logs instead of a URL), accepts only , and isn't a real option; it's . What it actually costs, per call Every inference call ships the page's full accessibility tree to the model. On 4geeks.com's homepage, that tree runs 67,347 characters, roughly 21,000 tokens , before your instruction is even added. That number is the story: cost tracks page size, not what you asked for. | Method | Prompt tokens | Completion tokens | Time | Model calls | | | | | | | | | 20,956 | 38–758 | 5–8 s | 1 | | by instruction | 16,006 | 32 | 9.8 s | 1 | | replay (cached action) | 0 | 0 | 0 s | 0 | | | 16,419 | 104 | 13.3 s | 2 | | self heal | 21,410 | 44 | 1.4 s | 1 | That row has a hidden cost nothing surfaces on its own: it makes two model calls, not one. The first extracts against your schema. The second checks its own work against a built in schema, , deciding whether the goal is "now accomplished." reports both calls as if they were one, so a budget built on "one call per " is wrong from the first run. An + sequence is three requests, not two, and that matters most on a free tier that counts requests. The one setting that actually moves the needle: Scoping a call to a subtree instead of handing over the whole page is the single biggest lever in the library, and it doesn't show up in the quickstart. | Variant | Input tokens | Change | | | | | | Whole page | 15,883–20,957 | baseline | | | 481–950 | −94% to −98% | | | 471–941 | −94% to −98% | | | 12,482–15,746 | −1% to −40%, page dependent | Scoping in beats excluding out . only helps as much as the excluded subtree happens to be large, which varies by page; gives you a predictable, near total cut every time. One API trap: takes an actual Locator instance , the object returns. Passing throws. That object shape is not the wire format. One tradeoff worth stating plainly: switches server side caching to for that call. The 94% token saving and a cache hit are mutually exclusive on the same request; you pick one per call, not both. The cache trap: it's a counter, not a similarity score defaults to 10 , and it's a repetition count: the identical instruction has to run against the identical page ten times before anything is ever served from cache. That's why throws , not a similarity failure; the field was never built to take a fraction. Most people reading "threshold" a