Bowmark AIdocs

How it works

Two calls. Read the library, then run a script against it on the live sites.

Every Bowmark task is the same two calls, in this order.

How a Bowmark call worksTwo steps. First the agent calls get_library and Bowmark returns typed function signatures, touching no website. Then the agent sends a script to run, Bowmark executes it against the live sites in a sandbox, and returns a structured result.1READ THE LIBRARYYour agentClaude Code · ChatGPTCursor · Codex · curlget_library({ query })typed signaturesBowmarkthe capability catalogNo website is touched.One read-only call.you write a script against them2RUN A SCRIPTThe scriptawait bowmark .flights.search()run({ script }){ ok, status, result }Bowmarksandboxed executorno fetch, no filesystemfetch · browserrowsThe live webkayak.comgoogle flightsexpedia.com

1 — Read the library

get_library({ query }) returns the callable functions that match what you want to do, with their argument shapes and return types.

It touches no website. It is one read-only call, and an unrecognized query returns a one-line index of the whole library rather than an error — so the check never dead-ends and never costs you an attempt.

const library = await get_library({ query: "flights" });

get_library is an MCP tool and HTTP endpoint only. It is not callable from your own Node or Python program — see the HTTP surface below, or use the CAPABILITIES.md and PROVIDERS.md lists to discover what you can call. npm i @bowmark/web gives you bowmark.<capability>.<fn>() and bowmark.providers.<site>.<fn>() — never a get_library export.

Pass what you want to do (flights, price a GPU) or a company if one was named (Kayak). You get what you asked about and nothing else.

Every response is bounded and says so when it is a slice. When it does, absence from the list proves nothing — the fix is a narrower query, not a conclusion.

2 — Run a script

You write plain async JavaScript against what came back, and run executes it in a sandbox. The sandbox reaches the live sites; your script cannot.

const { flights, warnings } = await bowmark.flights.search({
  from: "SFO",
  to: "JFK",
  depart: "2026-09-01",
});
return { flights: flights.slice(0, 3), warnings };

Bowmark picks the cheapest way to reach each site — a plain HTTP request where that works, a real browser only where the site forces one — and fans a capability out across every site that can serve it.

There is no third call. What comes back is { ok, status, result, logs, error, ms }.

The same two calls over plain HTTP

Nothing above needs an MCP client. The two calls are two HTTP endpoints, so an agent with only a shell or a fetch tool and an API key can use Bowmark with nothing installed.

curl -s "https://api.bowmark.ai/v1/library?query=flights" \
  -H "Authorization: Bearer $BOWMARK_API_KEY"
curl -s -X POST https://api.bowmark.ai/v1/run \
  -H "Authorization: Bearer $BOWMARK_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"script":"const { flights } = await bowmark.flights.search({from:\"SFO\",to:\"JFK\",depart:\"2026-09-01\"}); return flights.slice(0,3);"}'

Every request needs the key; one without it is refused with the steps to get one. See API keys.

Why a script instead of a tool call

One script, several calls, combined however the task needs. That is the thing you cannot do by driving a browser step by step, and it is the reason the surface is a language rather than a menu of tools.

const dates = ["2026-09-01", "2026-09-02", "2026-09-03"];
const runs = await Promise.all(
  dates.map((depart) => bowmark.flights.search({ from: "SFO", to: "JFK", depart })),
);
return runs.flatMap((r) => r.flights).sort((a, b) => a.price - b.price)[0];

Three days of fares, ranked, in one round trip.

The language itself is a separate topic — the globals you get, what is forbidden inside the sandbox, and how to read the response envelope are all in Scripting.

And you do not have to be an agent to do any of this. npm i @bowmark/web or pip install bowmark-web gives you the same library as typed functions in your own process, with autocomplete and no script string — see Scripting.

Two tiers in the library

Capabilities — bowmark.flights.search(...). The default. One call fans out across every site that can serve the task and routes around the ones that fail.

Providers — bowmark.providers.kayak.search(...). One specific site. Reach for one when you want that site's answer rather than the best available.

Prefer the capability unless you have a reason. It is the one that survives a site going down.

No provider? Read any page directly

A site with no dedicated Bowmark provider can still be read through the generic bowmark.read capability. It fetches any URL as a plain GET and escalates to a real browser only if the page requires one.

Call it through run() — never session(), and never the bare top-level bowmark client (it opens a session internally even for one call). read's rung is decided per call, so both of those refuse it with code: "rung_undeclared".

await run(`
  const page = await bowmark.read.page("https://example.com/blog/post-1");
  // \`content\` is markdown by default — pass { format: "html" } if you want the raw bytes.
  return { title: page.title, preview: page.content.slice(0, 200) };
`);

Both read.page (one URL) and read.pages (many URLs at once) take a fallback approach: they work through the cheapest reach available for that page, transparently escalating to a browser only when the site forces one. Pair them with the run call to read arbitrary sites without building a provider for each one.

A slow page has a fast-fail option, and it is worth knowing before you need it. timeoutMs is the budget for the whole read — both legs together, default 45s and capped at 55s, deliberately under the point at which a chat client gives up on a tool call. If you do not want to wait on a page at all, pass strategy: "fetch": it never opens a browser, comes back in about 200ms whatever the page is, and still sets escalationReason so you learn a browser was warranted instead of spending the budget discovering it.

const fast = await bowmark.read.page(url, { strategy: "fetch", timeoutMs: 15000 });
if (fast.escalationReason) {
  // Only now pay for the browser, knowingly.
  const full = await bowmark.read.page(url, { strategy: "browser", timeoutMs: 45000 });
}

content is capped at 200,000 characters unless you pass maxChars. Over the cap the read comes back with truncated: true, a content cut at … chars (page had …) warning, and the run's status partial — most often with format: "html", whose raw markup runs long. { maxChars: 600000 } returns the whole page. Keep maxChars × pages under about 500MB on a read.pages batch: a run has 1024MB for its whole script.

Reading several pages that each need a browser in ONE script is the shape that runs long — split those across separate run calls. A batch that does run long returns the pages that finished, with a row naming the budget for the ones that did not.

read.page still only reads. If the data you want only shows up after a click, a date picked on a calendar widget, or a form filled in, that's not a wider fetch — it's bowmark.browser_agent, a hosted browser agent that drives the page for you.

A login inside browser_agent is attended only — the person has to be watching. The agent completes the sign-in at watchUrl, then your script continues. For scripts that run unattended on a schedule, you need a typed provider with a signIn function and the request_secret / bowmark.secret() pattern described in Scripting § Storing a login for an unattended script.

See Scripting § When nothing else works: the browser agent.

No coverage at all yet? Say so — that's how new coverage gets built

Sometimes get_library has nothing for the task, and bowmark.read isn't enough because what you actually need is a structured capability — search, compare, book — for a site or a category Bowmark hasn't covered yet. That isn't a dead end.

Bowmark's library grows from what callers actually ask for. Every run you make is one data point, and calling report on it is the direct way to flag a miss instead of a silent one:

await report({ runId, report: "no provider for UK van-insurance comparison sites" });

runId is optional — pass the one run returned if this follows a call, or omit it to report a gap get_library itself couldn't fill. There's no ticket number and no promise of a date; what you get is that the miss is now counted, and coverage is prioritized by how many callers hit the same gap. A site with no coverage today can have it next week if enough real usage asks for it.

Reading this with an agent?

This page as plain text: /docs/how-it-works.md. The whole site as one file: https://bowmark.ai/llms-full.txt. Index of every page: https://bowmark.ai/llms.txt.