Three products in a row, on the node that serves the model. Code in a sandbox asks render for a screenshot of the demo app's billing page, hands the image to b3os/qwen3.5-9b through image_url, and asks it two things: which control switches the prices to yearly billing, and what the Business plan costs as shown. Vision then opens the page, presses the control the model named, and reads the Business price again. Each step is timed inside the sandbox.

The sandbox holds no key. Its grant (api.scopes) lets the code inside call render, infer and vision on http://127.0.0.1:7700, and the model preset (OPENAI_BASE_URL, OPENAI_API_KEY, B3OS_MODEL) is in its environment. The model reads the image from the screenshot's artifact URL, so the image never passes through the sandbox.

Check

Verified 2026-11-03: this recipe's last run passed against https://beta-api.b3os.org, label live-recorded (recorded from production), from commit a0816c07. The recipe harness runs it again, and a failed run takes this page out of the cookbook.

Run it

Inside the sandbox (Node 24 in the base template):

ts
const API = "http://127.0.0.1:7700";const PAGE = "https://beta.b3os.org/demo/billing/";const post = async (url: string, body: unknown, auth?: string): Promise<any> => ( await fetch(url, { method: "POST", headers: { "content-type": "application/json", ...(auth ? { authorization: auth } : {}) }, body: JSON.stringify(body), }) ).json();// 1. render: the page as an imageconst shot = await post(`${API}/v1/render/screenshots`, { url: PAGE, viewport: { width: 1280, height: 800 }, format: "jpeg",});// 2. infer: the model reads the imageconst read = await post( `${process.env.OPENAI_BASE_URL}/chat/completions`, { model: process.env.B3OS_MODEL, temperature: 0, chat_template_kwargs: { enable_thinking: false }, response_format: { type: "json_schema", json_schema: { name: "pricing_page", schema: { type: "object", properties: { toggle: { type: "string" }, business_monthly: { type: "string" } }, required: ["toggle", "business_monthly"], }, }, }, messages: [ { role: "user", content: [ { type: "text", text: "This is a screenshot of a pricing page. Give the visible text of the control that switches the prices to yearly billing, and the Business plan's price exactly as the image shows it.", }, { type: "image_url", image_url: { url: shot.artifacts[0].url } }, ], }, ], }, `Bearer ${process.env.OPENAI_API_KEY}`,);const seen = JSON.parse(read.choices[0].message.content);await fetch(`${API}/v1/render/screenshots/${shot.id}`, { method: "DELETE" }); // read; no need to keep it 7 days// 3. vision: press what the model named, then read the pageconst session = await post(`${API}/v1/vision/sessions`, {});const done = await post(`${API}/v1/vision/sessions/${session.id}/act`, { actions: [ { kind: "navigate", url: PAGE }, { kind: "click", target: { role: "button", name: seen.toggle } }, { kind: "markdown", only_main_content: true }, ],});await fetch(`${API}/v1/vision/sessions/${session.id}`, { method: "DELETE" });console.log(seen, done.results[2].markdown);

The recipe's run tries the control as a button, then as a link, then as any element with that text, and checks the price the page shows after the press. The model's two answers are free text from a page we publish, so a run whose answers or page change waits for a person's review before this page shows it.

Owner: Docs. Products: Render, Infer, Vision, Sandboxes.

Output of the last green run

json
{ "page_url": "https://beta.b3os.org/demo/billing/", "model": "b3os/qwen3.5-9b", "render_ms": 738.5, "image": { "width": 1280, "height": 800, "bytes": 74091 }, "screenshot_deleted": true, "model_ms": 1153.6, "prompt_tokens": 1051, "toggle": "Annual", "price_read": "$48 per month", "act_ms": 240.4, "pressed": true, "price_after": "$40", "read_sha256": "aa46f02b258763e5c79ea3632fbf98f69f00b697df2ac480fe2820e961a56b09", "session_closed": true, "total_ms": 2165.6, "create_ms": 432, "exec_ms": 2346.3, "deleted": true}

The outputs this recipe publishes (its recipe.json publish.outputs), with workspace identifiers redacted. The run's stdout, stderr and other outputs are not published.

Captured 2026-11-03 from commit a0816c07 , label live-recorded (recorded from production). Case recipes/render-read-act .

Ask a question... ⌘I