# Tools — giving an agent more than the model knows

The kinds of tool an agent can call mid-conversation, which ones you configure, and how an agent can even build its own tools and manage other agents.

Status: Shipped

A model on its own answers from what it was trained on. **Tools** are functions
the agent can call in the middle of a turn — to check the clock, ask you a
question, save a note, or fetch something over the web — and then keep going with
the result. You attach tools to an agent when you build it; the model decides
when to call them.

## Why it matters

Tools are the line between a chatbot and an agent. They are how an agent reaches
past the model's training data to _today's_ information, to your own saved
context, and to any HTTP API you can describe. Most of that power lives in one
configurable tool — **fetch_url** — so it's worth knowing what each kind does.

## The kinds

- **Current date & time** — a single built-in tool with no setup. It hands the
  model the current timestamp, which models otherwise can't know.
- **Ask** — the agent asks _you_ something mid-turn and waits: free text, a
  choice from a list, or a yes/no confirmation. Useful when a task needs a
  decision only you can make. No configuration.
- **Memory** — the agent saves and recalls notes in a **store** you name, so
  context can survive across conversations. You set the store's name and, optionally,
  the list of keys it may use; when you attach it to an agent you can also gate
  whether that agent may write or delete, so one agent gets read-only access to a
  store another agent fills.
- **Fetch a URL** — the configurable workhorse. You define an HTTP request: a URL
  written as a template with typed variables the model fills in (each with a type,
  an optional default, and required/array flags), the method, headers, a request
  body (a JSON one written in placeholders or in MiniJinja), an optional API key
  from your saved secrets, retry behaviour for rate-limited
  responses, error handling that flags failed responses (and can render a custom
  message from the error body instead of handing back a raw error page), and
  output filters that shape the response before the model sees it — with a
  template, with a jq program, or with a model of your choosing — and a cost
  expression that reads what the endpoint charged so a paid API's fee joins your
  conversation totals. You can also pin the response's
  content-type — say JSON or Markdown, or any type you name.
  The tool derives an `Accept` header from those types and treats a reply that
  arrives as anything else as an error, so a misconfigured endpoint's HTML error
  page never reaches the model as if it were the data you asked for. The headers
  list shows every header the request adds for you — that derived `Accept`, the
  `Content-Type` for your body, and the default `User-Agent` — each with its
  exact value and an Override button, so you can send your own value or clear it
  to suppress the header. One fetch tool is one endpoint; add as
  many as you need. This is also how an agent searches the web — point a fetch
  tool at a search API's endpoint (there is no separate search tool).
- **Conversation title** — the agent's own model names the conversation and sums
  it up while it answers, instead of a second model reading the whole
  conversation again afterwards. You give the tool a name and, if you like, your
  own description: the description is the only thing that tells the model when
  to call it, so it is yours to tune, and left empty it uses the app's, which
  asks for a call on the first exchange and when the topic moves on. A title you
  typed yourself is never replaced. With automatic titles also switched on, they
  step in once a conversation has grown past their interval without a call.
- **Call another agent** — hand a message to one of a fixed set of other agents
  in the workspace and get its reply back, without leaving your conversation. You
  choose exactly which agents it may reach when you attach it — it can call those
  and no others — so delegating a sub-task to a specialist agent stays inside the
  boundary you set. An empty list calls no one.
- **Manage tools** — self-configuration. Meta-tools that let the agent itself
  list, inspect, test, save (`tools-save_fetch_url`,
  `tools-save_conversation_title`), and delete tools, and point a tool's API key
  at a saved secret (`tools-link_secret`) or clear it (`tools-unlink_secret`).
  This is what makes an agent that builds and fixes its own fetch tools, or sets
  up a title tool for the agents it creates, possible; a
  deletion always asks you first, and so does a secret about to reach a new host.
  `tools-request_secret` and `tools-copy_secret` still work for agents that have
  them but are being retired: they are no longer offered when you attach tools,
  and the agent's page names what replaces them.
- **Manage agents** — an agent can list, inspect, create, edit, and retire the
  other agents in its workspace, and even test one, carry the test conversation on
  with a follow-up message, and answer the questions it asks — so an agent that sets
  up other agents can build and try them out. `agents-save` refuses an allowlist
  that would let agents call each other in a loop, naming them in call order.
- **Manage providers** — tools that let an agent list your configured providers
  and browse each one's models, so an agent building other agents can choose a
  valid provider and model instead of guessing (your API keys stay hidden). It
  can set a provider's icon, which reaches nothing a request is made of, and —
  with `providers-save` attached — add or change a provider, which asks you
  first whenever the change decides where requests go. `providers-link_secret`
  points a provider at a saved secret and `providers-unlink_secret` clears it;
  the agent never sees a secret's value.
- **Manage saved secrets** — `secrets-list` lists your saved secrets by name, with
  the hosts they are sent to and the providers and fetch tools that use them,
  never their values. `secrets-save` has you create a secret, changes one's name,
  hint or link, or has you type a secret's new value in a prompt that lists
  everything it reaches;
  `secrets-delete` deletes one after you approve a prompt naming everything that
  loses it. No agent tool revokes a secret — that stays on the Secrets page.
- **Manage constants** — `constants-list` lists the
  [constants](#constants-an-account-id-typed-once) the agent's workspace reaches,
  with the text in effect there and what uses each one. `constants-save` creates or
  edits a constant, or gives a global constant this workspace's own text.
  `constants-override_for_agent` gives one agent of the workspace its own text for a
  constant, or hands the constant back to the workspace's text. Linking sits
  with what is linked, so attaching one does not grant the other:
  `tools-link_constant` links a fetch tool's `{constants.…}` slot to a constant and
  `tools-unlink_constant` clears it, and `providers-link_constant` and
  `providers-unlink_constant` do the same for a provider. Changing the text of a
  constant something uses, and every link, asks you first and shows each host the
  change moves requests to; clearing a slot does not ask. An agent never sees or
  names a constant of another workspace.

## How it works

Open an agent and attach the tools it should have. The ones that need setup —
a memory store's name, a fetch request, a conversation title tool's description —
you fill in on a form; the rest just work
once attached. Some tools take per-agent options right where you attach them (the
memory write/delete gates, an agent-call tool's allowlist). When the model calls
a tool during a conversation, the call and its result become part of the turn,
and the agent continues with what it learned.

![An agent with a fetch tool attached, shown in the agent's Tools section](/images/agent_fetch_tool.png)

Every configured tool can carry an **icon** — one of a curated set of glyphs, an
emoji, or a link to artwork on the web — which it then wears everywhere it
appears: in the tools list, in the picker when you attach it to an agent, and on
its call in the middle of a conversation. That last one is the point: a turn that
calls four tools reads at a glance instead of by name. A tool with no icon of its
own wears its kind's, so fetch tools, memory tools and the rest are still
distinguishable without setting anything.

The picker has a tab per source — **Icons**, **Emoji**, **Link**, and
**Default** for wearing the kind's own mark again — with a preview at the top
showing what you are about to set. Choosing inside a tab selects; **Use icon**
(**Use default** on the Default tab) applies it and closes the picker, so you can
look around without changing
anything.

For a fetch tool, the icon you probably want is the endpoint's own logo, and
**Find automatically** goes and gets it. A site usually publishes several marks —
a small legacy favicon, a large one for phone home screens, sometimes a logo made
for dark backgrounds — so the search shows you all of them, grouped by the host
each came from: the endpoint's own host, the domain directly above it, and the
domain that host is registered under — so an endpoint on a gateway subdomain
still finds the company's mark. The
likeliest one is taken for you and its address appears in the field below the
marks; pick any other one to take it instead. You can also paste an address
yourself, and add a second one for dark mode if the mark needs it — glyphs and
emoji don't, since one is tinted to the theme and the other sits on a plate.
Whichever way it arrives, the address is checked when you accept it, so an icon
that doesn't load is refused there and then rather than becoming a gap in a list.
Only the address travels between your devices; each one fetches the artwork
itself, and that only ever happens for a tool you gave a link to.

An agent with the **Manage tools** meta-tools can set a tool's icon too — pick a
glyph, an emoji or a link, or ask it to find the endpoint's own mark — under the
same rules: nothing is stored unchecked, and changing a global tool's icon asks
you first.

An agent can also look before it picks. Given any address, the **icon search**
meta-tool (`icons-discover`) reports the marks published there — each one's link, the host that
published it, and how large and how dark the artwork is — without storing
anything. That is the same search the button above runs, with the choice left
open rather than made: the agent reads the candidates, picks one, and sets it
with the tool or provider icon tool. Handing it the address of artwork directly
checks that one instead, which is how an agent tells a usable mark from a broken
link before it sets anything.

### Reading a picture

One built-in tool, **Fetch image**, reads the image at an address you or the agent
names — a chart, a screenshot, a diagram, a photo. It is separate from the fetch
tools you configure yourself because those are locked to the endpoint they were
set up for: a fetch tool can only fill in the blanks of its own URL, so none of
them can open a picture somewhere else. This one takes the whole address.

The picture is stored with the conversation and shown in the result, so you see
what the agent saw. It is also handed to the model as an image rather than as a
description of one, which is what lets an agent read a chart or check a
screenshot. A model without vision gets a note in its place instead of an error,
and the conversation carries on.

It reads PNG, JPEG, GIF, BMP and WebP, and refuses SVG, anything over 8 MB, and
any address that turns out not to hold an image at all — saying which, so the
agent can try a different link rather than guess.

Tools are scoped to workspaces: a tool can be **global** (available everywhere) or
private to one workspace, and an agent only ever runs the tools in its own
workspace plus the global set. A new tool or a copy can go into any workspace, not
only the one you are looking at, so copying is how a tool moves between workspaces.
A workspace tool may share its name with a tool in another workspace, but not with a
global one; a fetch endpoint's name takes at most 54 characters, a conversation
title tool's 45. The tools that ship
with the app are listed as
**Built-in** rather than global — they are everywhere for the same reason a menu
item is, and there is no scope on them to change. You can test any tool, built-in
ones included, in isolation: the floating button on its detail page runs it with
arguments you provide and shows the raw result — so a misbehaving tool doesn't need
a full conversation to debug. Editing, copying, exporting and deleting sit in the
page's ⋮ menu; **Export** saves the tool, with any provider it names, as a
`.bylm.json` file (see [Sharing](/docs/features/sharing)). A tool form with unsaved changes
asks before you leave it.
That page also says when the tool was created and, once you have changed it, when
it was last updated. Both dates ride the tool itself, so they read the same on
every device it syncs to. The tools that ship with the app keep no record of their
own, and neither do tools saved before the app began noting the dates, so those
show nothing.

### Star the tools you reach for

Tools are offered in one order everywhere — on the **Tools** screen and in the picker you get when
adding a tool to an agent: starred first, then newest to oldest. Tap the star on a tool's row to pin
it, and it heads both lists — the picker shows the star beside the name too, so a pinned tool reads
as pinned rather than merely early. The pencil beside the star opens that tool's edit form
directly, for when the edit is what you came for. The star is kept on the tool itself, so it
follows the tool when it moves to another workspace and reads the same on every device the tool
syncs to. The tools that ship with the app keep no record of their own, so they carry no star and
sit at the end of the list, by name.

### Copy a tool you already have

A second endpoint on the same service usually differs from the first in a line or
two — another path, one more header. **Copy** in the tool page's menu opens the create
form on a duplicate of it: same URL, method, body, headers, variables and
response handling, with the name suffixed `_copy`. A tool name can only hold
letters, digits, `_` and `-`, so the suffix is one the form accepts as it stands —
but it is a placeholder rather than a suggestion, and the point of the copy is
usually a name that says what the second endpoint is for. Nothing is written until
you save.

The copy uses the same saved secret as the original rather than a second copy of
it, so a later rotation reaches both. Clearing the field gives the copy no API
key, and choosing or creating another secret in the selector leaves the
original's alone. See [Saved secrets](/docs/features/providers#saved-secrets) for picking a
secret another provider or tool uses and for the Secrets page.

### Saved secrets in a request

A fetch tool places its API key as `{secrets.apiKey}` in the URL, a header or the
body, and any other secret the endpoint needs — an organization token, a signing
secret — as `{secrets.<name>}`, such as `X-Org: {secrets.org}`. The key is chosen in
the **API key** field; every other slot the request places gets a field under
**Saved secrets** on the tool's form, working like the key's. The value stays in
your device's keystore and is filled in only when the tool runs; what the app logs
or shows keeps the placeholder.

- **Where it goes.** A secret goes whole in the path, the query, a header or the
  body, as `{secrets.org}` or `{+secrets.org}`. One in the host or a fragment, cut
  short as `{secrets.org:4}`, or inside an operator that writes its name, such as
  `{?q,secrets.org}`, is refused on save. In the URL a secret is percent-encoded
  like any value; `{+…}` leaves reserved characters as they are.
- **When one is missing.** A request whose secret is unlinked or not on this device
  stops before it is sent and names the slot. Only the API key can be optional.
- **The older `{apiKey}`.** It still works where a secret may go, and is still put
  into the request exactly as stored, except that a header drops any line break in
  it. One in the host, a fragment or a cut-short
  expression is refused like any secret, and a stored tool with one there stops
  before its request is sent. A tool whose host or port is a constant cannot use it
  at all. The form and `tools-save` say to write `{secrets.apiKey}` instead, which
  fills the same key.

### Constants: an account id typed once

An id that several tools need — a Cloudflare account, an organization, a region,
a resource name — can be kept once as a constant under **Settings › Constants**
and placed in a fetch tool's URL, headers or body with `{constants.<name>}`, where
`<name>` is any placeholder name you choose, such as `{constants.account}`: letters,
digits and `_`, not starting with a digit, the same rule as a `{secrets.<name>}`. Each
placeholder the request uses gets a field under **Placeholders** on the tool's form;
tap it to choose a constant, make a new one, or change the value of a listed one,
without leaving the form. A constant is not a secret: its value syncs between your
devices as written and shows wherever the request does.

- **Where to find it.** A constant can carry a hint on where its value is found and
  a link to the page that shows it, such as the account page an id is read from.
  Both are shown wherever you are asked for the value, and travel with it in a
  shared file. The link opens in your browser; the app never requests it.
- **Scope.** A constant is global, or private to the Default workspace or to one
  workspace, like a tool. A workspace tool can link its workspace's constants and
  the global ones; a global tool links global constants.
- **Overrides.** A workspace can override a global constant with its own value. A
  tool that links the global constant then sends the override's value whenever it
  runs in that workspace, and the global value everywhere else. An agent can
  override a constant too, over its workspace's value; see [Agents](/docs/features/agents).
- **In the host.** A constant can be the host or part of it, as long as it fills
  whole labels on its own: `https://{constants.resource}.openai.azure.com` works,
  `https://api{constants.region}.example.com` is refused on save. A constant placed
  in a host must be letters, digits, hyphens and dots. A constant can also be the
  whole port, as a number from 1 to 65535.
- **In output filters.** A filter can read a constant too: `$constants.account` in
  a jq program, `{{constants.account}}` in an LLM filter's Mustache prompts and
  `{{$constants.account}}` in a Mustache template filter, `{{ constants.account }}`
  in MiniJinja for either. Its placeholders get fields beside
  the request's. Read each constant by its name; a filter that reads them whole or
  by a computed name, such as `$constants[.key]`, is refused on save.
- **When one is missing.** A request whose constant is unlinked, deleted or out of
  scope stops before it is sent and names the placeholder.

A saved secret only follows a constant to a new host once this device approves
that host, so changing a constant that sets the host stops a tool that sends a
secret until you approve the new host on the secret's page.

### Request metadata: who is asking

A fetch tool's URL, headers and body can also place values the app fills for each
request: which conversation, agent and workspace the call is made for, and which
app makes it. The URL, header and body fields offer each one as a chip.

| Name | Holds |
|---|---|
| `{meta.conversation.id}` | The conversation's id |
| `{meta.agent.id}`, `{meta.agent.name}` | The agent that made the call |
| `{meta.workspace.id}`, `{meta.workspace.name}` | That agent's workspace; Default has no id |
| `{meta.app.name}`, `{meta.app.shortName}`, `{meta.app.version}` | `Bring Your LM`, `BYLM`, and this build's version |

- **Not for the model.** These are not call arguments. The model is never asked
  for them, and an argument it sends under one of these names is dropped.
- **Where they go.** In the path, the query, a header or the body, never in the
  host: a name you can rename must not decide which server a request reaches. A
  header carries a name percent-encoded outside plain ASCII, so `Café` goes out as
  `Caf%C3%A9`. A JSON body takes them inside strings.
- **When there is no conversation.** On the tool's test page, the app's fields and
  the workspace the app is showing are filled, and the conversation and agent are
  empty. While the app shows all workspaces, the workspace fields are empty too.
- **An agent called from a conversation.** Its tool calls name its own
  conversation and its own agent, not the caller's.
- **Spelling.** A name outside the table, such as `{meta.agent.title}`, is
  refused on save.

The default `User-Agent` is one the app writes for you. To send your own built from
these names, override it in the headers list:

```text
User-Agent: {meta.app.shortName}/{meta.app.version} ({meta.agent.name})
```

### What a template field can read

Every template field of a fetch tool — the URL, each header, the body, the error
message template and each filter's template or prompts — lists under it the names it
can read, as chips that say what each holds. Tap one to insert it at the cursor. A
Model filter's prompts also offer each of the tool's arguments, as `{{args.<name>}}`.

### A JSON body written in MiniJinja

A JSON body can be written in placeholders, as above, or in MiniJinja, picked
above the body field. MiniJinja is the choice when the body has to be built: a
list from an array argument, a field present only when an argument is, a value
with a fallback.

```jinja
{"query": {{ q | tojson }}, "limit": {{ limit | default(10) }},
 "region": {{ constants.region | tojson }}{% if tags is defined %},
 "tags": {{ tags | tojson }}{% endif %}}
```

- **What it reads.** Each argument by its name, a saved secret as
  `secrets.<name>`, a constant as `constants.<name>`, and the request metadata
  under `meta`, such as `meta.agent.name`. An argument the agent leaves out takes
  its declared default, or else is undefined.
- **Write every value with `tojson`.** It quotes and escapes the value, so a quote
  in what the agent sends cannot break the body. An undefined value goes out as
  `null`; printed any other way, it fails the call and nothing is sent.
- **Only where a body is sent.** A GET or HEAD request sends no body, so a body kept
  from another method offers the model none of its arguments.
- **On a device still on 2026.09.12.** That version cannot read a MiniJinja body: the
  tool sends its request there without one, and saving the tool on that device
  removes the body.

### Shaping a response before the model reads it

A fetch endpoint's **output filters** run in order over the response body, each
one's output feeding the next, and only the last one's output reaches the model.
There are three kinds:

- **Template** — a template rendered over the JSON the endpoint returned, so a
  payload full of fields the model would have to parse becomes the one sentence,
  table, or reshaped object you wanted. It costs nothing and adds no delay. It is
  written in Mustache or in MiniJinja, chosen above the template.
- **jq** — a [jq](https://jqlang.org/) program run over the response, for the
  work a template cannot do: filtering an array, computing a total, picking the
  three fields out of forty that the model actually needs. Exact, instant, and
  free. A jq program is a stream, so you also choose what happens when it
  produces more than one value — take the first, collect them into an array, or
  put each on its own line — and a program that selects nothing can be treated as
  an error, which is how you catch a response whose shape has quietly changed.
  Programs run with no file, network, or environment access: they see the
  response body as input, its headers as `$headers`, the arguments the agent
  called the tool with as `$args`, the URL template, method, status and content
  type as `$attrs`, the tool's constants as `$constants`, and nothing else. A
  **Emit strings unquoted** switch returns a string the way `jq -r` does.
- **Model** — asks a model one question about the response and passes its answer
  on instead: summarise the page, pull out the single fact, rewrite a payload the
  endpoint won't rewrite for you. It is one question with no tools, so it cannot
  wander off doing its own research the way an agent with a fetch tool would.

Filters compose, and the useful pairing is jq before Template: select the data
exactly, then render prose around it.

Order matters, because Template and jq both read JSON. Every filter has an
**Output type** — Plain text, Markdown or JSON — which says what it hands on and
how the conversation renders it. A Template or jq filter placed after one whose
output type is not JSON is refused on save, with the problem shown at that earlier
filter's output type, since that is what to change. A Model filter set to JSON
that answers in prose anyway still fails the call, and the failure says which
stage produced the text rather than only which stage choked on it.

The dependable way to put a Model filter in front of a jq or Template stage is to
give it an **answer schema** — a JSON Schema the provider holds the model's reply
to, so the answer arrives as data rather than as a sentence wrapped around a code
fence. Leave it empty to ask for prose. **Hold the answer to the schema exactly**
asks the provider for strict adherence; some providers refuse a strict schema that
does not set `"additionalProperties": false` and list every property as required.
If your provider cannot constrain a
reply, the call fails saying so instead of quietly asking without the schema.
Any schema of JSON Schema draft 2020-12 is accepted; which keywords are honoured
is up to your provider, and some refuse a schema that uses one they do not
support.

A **MiniJinja** template uses Jinja's syntax, with a loop and a condition each
written as what they are — `{% for item in body.items %}` and `{% if body.next %}`
— and every name it reads spelled out from where it comes from: the response as
`body`, the agent's arguments as `args`, the exchange as `request` and
`response`, your constants as `constants`. The status is a number and an argument
keeps the type the agent gave it, so `{% if response.status >= 400 %}` compares as
written. Saving checks those names, so a
misspelt one is caught before the tool runs, and a value the response does not
have fails the call instead of printing nothing: test it with
`{% if body.next is defined %}` or give it a fallback with
`{{ body.next | default('none') }}`. A Mustache template reads the response's
fields at the top level and prints nothing for one that is missing.

Some of what an endpoint says is not in the body at all. Whether there is a next
page of results is the `Link` header; what is left of your rate limit, when the
data was last modified. All three filters can read them, each in its own
spelling: a jq program indexes `$headers["link"]` by lower-cased name, a Model
filter's prompts quote `{{headers.link}}` in Mustache or `{{ response.headers.link }}`
in MiniJinja, and a Template writes
`{{$response.headers.link}}` in Mustache or `{{ response.headers.link }}` in
MiniJinja. The Template filter's `$` is not decoration — that
filter renders over the response body itself, so its plain names are the body's
own fields, and the prefix is what keeps an endpoint that answers with a
`headers` field of its own from being confused with the headers it arrived with.
The same shape carries the rest of the exchange there: `{{$request.url}}`,
`{{$request.method}}`, `{{$response.status}}`, `{{$response.mimeType}}`.

Four headers are held back whatever the endpoint sends —
`Set-Cookie`, `Authorization`, `WWW-Authenticate` and `Proxy-Authenticate` —
because a filter's output goes to the model word for word and a Model filter's
prompt travels to whichever provider runs it, so a session cookie that reached a
filter has left the app. Nothing about the _request_ is there either: not the
expanded URL, not the headers you set, not your API key. You can still read every
header the endpoint sent, dropped ones included, on the call's **Response
details** screen.

Both prompts of a Model filter are templates too, over the response body, the
arguments the agent called the tool with, and the request and response metadata —
so the filter can ask about what was actually requested ("the user asked about
`{{args.city}}` — answer from this page"). You pick which model runs it with the
same provider and model pickers an agent uses, so a small local model is as
available as a hosted one, and the URL it is shown is the template you wrote,
with its secrets left unexpanded so they never travel into a prompt. A filter
needs a model to be saved, and a new one runs at its model's default temperature.
Like an agent, it can name what its model must handle —
_Image input_, _Reasoning_ — and an answer schema adds _Structured output_ on its
own; the model list opens narrowed to the models that advertise them.

The two prompts are written in Mustache or in MiniJinja, chosen above the
instruction, and both are always in the same one. In MiniJinja they read the text
the filter is given as `{{ body }}` and every other name as a Template filter's
MiniJinja does — `args`, `request`, `response`, `constants` — so
`{% if args.lang is defined %}Answer in {{ args.lang }}.{% endif %}` adds a line
only when the agent passed a language. Saving checks the names both prompts read,
and a value one reads that is missing fails the call rather than sending the model
an empty gap.

Two settings keep it inside the tool call: how much of the response the model is
shown (20 000 characters by default — the biggest lever on how long it takes) and
a timeout (10 minutes — long enough for a reasoning model to work through a big
body). If the call fails or times out, the tool call fails; the raw page is never
quietly handed to the agent instead. What the filter's model call costs is
counted into the conversation's usage totals like any other call.

### What the endpoint charged

Plenty of useful APIs bill per call, and most of them say what a call cost in
their reply — `usage.cost` in the body, an `x-credits-used` header, something of
that shape. An endpoint can carry a **cost expression**: a jq program that reads
that number out of the exchange, so the fee lands in the conversation's totals
and on the tool's tile beside what the model spent. Until now that side of the
bill was simply invisible.

It reads the response body and returns a number — `.usage.cost` — exactly like a
jq output filter on the same tool, so there is one path syntax to learn rather
than two. The rest of the response arrives as variables: `$status`, `$headers`
(response headers only, names lower-cased) and `$bodyText` for an endpoint that
doesn't answer in JSON. So a metered header is
`$headers["x-credits-used"] | tonumber`, "only charge me when the call
succeeded" is `select($status == 200) | .usage.cost`, and returning nothing
means the call cost nothing to report. If the endpoint bills in its own credits
rather than dollars, say what a credit costs and the conversion happens once,
exactly.

It reads the fee before anything else classifies the response, so a call the
endpoint refused **and charged you for** is still counted — a 402 that bills you
is a bill. And nothing about the request reaches the expression: not the expanded
URL, not the request headers, not your API key.

An expression that fails records nothing and leaves the response exactly as it
would have been, so a change at the endpoint costs you accuracy rather than the
call. If you would rather find out immediately, you can have a failing expression
fail the call instead.

Either way the call keeps a record of what the expression did, because a fee
that never arrives is the hard case: an expression reading a field the response
doesn't have looks exactly like an endpoint that charged nothing. Open
**Response details** on a call from a paid endpoint and the first block is the
cost expression — whether it read a fee, read none, or failed and why, with the
program itself underneath. An agent testing the tool is told the same thing, so
it can tell a free call from a broken expression without guessing.

### Where the money went

A single total can't tell you whether it was the endpoint's fee or a Model filter
that turned out to be expensive — which is the one thing you want to know while
you are still writing either of them. So a call that spent in more than one place
itemises it: the tool's tile in the conversation grows a **Spend** row listing
each contributor, the endpoint's fee and each filter stage that ran a model, with
its tokens, its price, and the total underneath.

The same breakdown sits on the tool's own test screen, so a cost expression can
be checked by running the endpoint once instead of by starting a conversation.
A call that spent and then failed still shows what it spent — on the test
screen, on the tile in the conversation, and in the totals — because a paid
endpoint that charged you and then returned something unusable has still
charged you. An agent testing a tool
gets it as JSON in the same shape, with each amount in dollars and in the exact
whole number the app counts in — enough to check a credits-per-call conversion
rather than eyeball it.

When a fetch tool has actually run, the tool's tile in the conversation carries
the response status, and opening the tile adds a **Response details** row beside
its Arguments and Result. That opens the whole
exchange on its own screen: the response headers, the raw body exactly as it
arrived, and the output of each filter stage in turn, so you can see where the
data you expected stopped being there. A Model filter also shows the exact
messages it sent, above its answer, headed with which model ran and what it cost.
It's syntax highlighted by content type, and anything that had to be cut says so
along with the size it was cut from — raise the endpoint's capture limit (in its
**Response capture** section) when the part you need falls past the cut, or turn
capture off for an endpoint you never debug.

You do not have to start a conversation to see any of that. Every tool has a
**Test tool** screen — reached from its detail page — that runs it for real with
arguments you type, through the same retry loop a real turn uses, and then shows
the whole record of the call: anything the tool published about itself while it
worked, every attempt it made with what each one failed on and how long the wait
before the next was, what it spent, and the same **Response details** screen with
the raw body and every filter stage. It is the shortest way to find out whether a
jq program, a Model filter or a cost expression does what you meant, because a
call and its evidence are one button apart.

Each block on that screen has its own switches. **Format for reading**
re-indents a body that arrived minified — JSON, HTML, XML and SVG — and switches
back to the bytes as captured; it only appears where there is something to
re-indent. A body the capture limit cut off mid-document still re-indents, up to
where it stops. **Render as Markdown** shows a block the endpoint sent as
Markdown — or a filter stage that produced it — as formatted prose instead of
source, and appears only on those blocks. **Wrap long lines** folds a body that
would otherwise scroll sideways. All of them are per block, so you can wrap the
raw response while leaving a filter stage's output as it is.

When an agent builds a fetch endpoint, it asks before the endpoint exists: the
prompt names the host the tool would send to and shows the URL, because whoever
operates that host sees everything the endpoint sends them — the URL, the
headers and the body, including values filled in from the conversation. It asks
again if a later edit points the endpoint at a different host, and it does not
ask for the edits in between: what you accepted is the party, not the wording of
one request. An agent cannot leave the host to a call variable either — the
address has to be written out, or there would be nothing to put to you. You are
asked the same thing when you build an endpoint yourself — a box above **Save** on
the create form — and on the edit form when a changed URL points at a new host: the dialog names both
hosts, and Cancel keeps the tool as it was.

These prompts, and the ones before a deletion or a change to a global tool, are
headed as needing your approval, and only you can give it: they wait for you even
inside an agent another agent is testing, and a yes counts only for the prompt it
was given to.

Any secret a tool needs (an API key for a fetch endpoint) is a saved secret kept
in your device's OS keystore and substituted into the request at call time — it is
never written into the tool's visible configuration, and never shown to the
model. The **API key** field is a selector, as on the provider form — see
[Saved secrets](/docs/features/providers#saved-secrets): it shows the secret the tool
uses, and its dialog picks another, creates one, or changes one's value.

When an agent needs a secret for a tool it built, it asks you to create one
(`secrets-save`) and then links it (`tools-link_secret`) — or links a secret you
already have. The agent can propose a hint on where to get the value and a link
to the page where one is made, and the prompt shows both, for you to keep or
change. You type the value into a prompt; the agent learns only the secret's
name. Linking asks you first, naming the host the secret would be sent
to, unless it already goes there. Declining writes nothing.

### When a tool isn't available here

Tools sync between your devices, and each device can only run the kinds its own
version of the app knows about. A tool can therefore be present on one device and
unusable on another — and a tool you delete stays referenced by the agents and
conversations that already had it attached. The app names all of that rather than
going quiet:

- **In the tools list**, a tool this device cannot open sits under _Not available
  on this device_ with the reason, instead of being invisible.
- **On an agent**, an attached tool the agent's turns cannot use is marked
  wherever the agent is shown, so what you see is what a turn will actually run.
- **In a conversation**, a banner above the composer names each tool that is
  missing and why. The conversation still answers — one tool going missing must
  not stop you talking to the agent — but it no longer does so silently.
- **Before a delete**, the confirmation names the agents and conversations that
  still use the tool, whether you are deleting it yourself or an agent asked to.

Each reason comes with the action that helps. A tool of a kind only a newer
version knows asks you to update; a deleted one says it is not available on this
device. So does a fetch tool with a template in a syntax only a newer version
knows: it is neither run nor edited here, rather than sending a request built from
a template read the wrong way. A tool configured with an output filter your version cannot run is kept
exactly as configured — nothing here rewrites it — and is left out of the turn
rather than being run without its filter.

## What it doesn't do yet

- **No standalone web-search tool.** Search is a fetch tool pointed at a search
  API you choose, not a one-tap "search the web" button.
- **Agents build only fetch tools.** The self-configuration tools create and edit
  fetch endpoints; a memory or conversation title tool is set up on its form, and
  the fixed built-in
  tools aren't things an agent can author.
- **Filters can't read request metadata.** `{meta.*}` names work in a request's
  URL, headers and body. Output filters, error templates and the cost expression
  can't read them yet.
- **Error templates can't read constants.** Request templates and output filters
  read them; an error message template reads only the status, the reason and the
  error body.
- **Only the API key can be optional.** Every other saved secret a request places
  must be linked and on the device, or the request stops.
- **A JSON body is the only one MiniJinja writes.** A form body is a URI template
  and a plain-text body is written in placeholders.

More in [Features](/docs/features): [Providers — bring your own LLM](/docs/features/providers) · [On-device models — run fully offline](/docs/features/embedded-models) · [Agents — the thing you build](/docs/features/agents) · [Workspaces — keep your work in scopes](/docs/features/workspaces) · [Conversations — how a turn runs](/docs/features/conversations) · [Attachments — images and files in a message](/docs/features/attachments) · [Context management — what the model still sees](/docs/features/context-management) · [Background turns — replies that keep running](/docs/features/background-turns) · [Local-first & private by design](/docs/features/local-first) · [Sync — your data across your devices](/docs/features/sync) · [Sharing — move agents, tools and providers as a file](/docs/features/sharing) · [Marketplace — find and publish shared agents, tools and providers](/docs/features/marketplace) · [Running a Couchbase Edge Server](/docs/features/couchbase-edge-server) · [Spell checking — catch typos while you write](/docs/features/spell-check)

---

[Bring Your LM](/) · [Docs](/docs/features) · [Marketplace](/marketplace)

[Articles](/articles) · [Releases](/releases) · [Bundle Format](/marketplace/bundle-format) · [Marketplace Terms](/terms) · [App Privacy](/privacy) · [Website Privacy](/website-privacy) · [Open Source](/open-source) · [llms.txt](/llms.txt)
