# How it works (/docs/how-it-works)

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

<FlowDiagram />

## 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.

```js
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](#the-same-two-calls-over-plain-http) below, or use the [CAPABILITIES.md](https://github.com/bowmark-ai/web/blob/main/CAPABILITIES.md) and [PROVIDERS.md](https://github.com/bowmark-ai/web/blob/main/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.

```js
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.

```sh
curl -s "https://api.bowmark.ai/v1/library?query=flights" \
  -H "Authorization: Bearer $BOWMARK_API_KEY"
```

```sh
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](/docs/installation#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.

```js
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](/docs/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](/docs/scripting#from-your-own-code).

## 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"`.

```js
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.

```js
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](/docs/scripting#storing-a-login-for-an-unattended-script).

See [Scripting § When nothing else works: the browser
agent](/docs/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:

```js
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.
