InlayDocs

Server tools

The platform's built-in tools — web search, URL fetch, code execution — and the contract they share.

A server tool runs on Inlay, not in your app. The agent calls it, the server executes it, and your client sees the call as an observability row — no handler, no pause. (The other kind — tools your app implements — is Client tools.)

Attach a server tool to an llm or for_each_llm node by name in its tools list, exactly like a client tool. Three are built in:

ToolWhat it doesMetered as
web.searchWeb search (Tavily-backed), curated results1 credit per basic/fast/ultra-fast search, 2 per advanced
url.fetchRead one URL as clean text1 call
code.execRun a Starlark snippet in a deny-all sandboxSeconds (rounded up)

Two more families are server tools too — MCP connectors (mcp.<alias>.<tool>) and subagents-as-tools (agent.<alias>) — covered by MCP connectors and the subagent picker on the canvas.

web.search

Searches the web and returns curated, capped results. Requires the deployment to have a Tavily key configured (INLAY_TAVILY_API_KEY) — without it the tool is not registered at all, so a graph referencing it fails at build with unknown-tool rather than failing mid-run.

Args: query (required), max_results (1–20, default 5), search_depth (basic default, advanced, fast, ultra-fast), topic (general/news/finance), include_answer, include_domains, exclude_domains. Results are aggressively bounded (per-result and total content caps, a truncated marker when they bite) so a search cannot blow up the model's context.

Node config (tool_configs):

{ "web.search": { "search_depth_cap": "basic" } }

search_depth_cap: "basic" forbids the 2-credit advanced depth — the model asking for it gets a teachable invalid-args error it can correct.

url.fetch

A reader, not an HTTP client: it fetches one absolute http(s) URL and returns the page as clean text (HTML is converted, JSON pretty-printed, other content types refused), delivered in a clearly-delimited untrusted-content block. Args: url (required), max_chars (1–50,000, default 12,000). No method, headers, or body — it cannot POST. No config surface.

It rides the strict egress guard (below) with no exceptions, ever.

code.exec

Runs a snippet in an embedded Starlark sandbox — deterministic arithmetic, parsing, reshaping — with no filesystem, network, or environment access. Args: source (required, ≤ 64 KiB), input (a JSON string exposed as the INPUT global), language ("starlark" only). The script's top-level result variable is the return value; stdout is capped and returned alongside.

Node config tightens the sandbox per node (defaults shown):

{ "code.exec": { "fuel_cap": 100000, "heap_bytes": 16777216, "wall_timeout_ms": 5000 } }

Exhausting fuel/heap/time is a curated tool error the model can react to, not a run failure. code.exec takes no secrets in v1 — a secret_ref argument is rejected at validation.

tool_configs

Per-node configuration lives in the node's tool_configs — a map from tool name to its config object:

{
  "tools": ["web.search", "code.exec"],
  "tool_configs": {
    "web.search": { "search_depth_cap": "basic" },
    "code.exec": { "wall_timeout_ms": 10000 }
  }
}

Configs are validated at graph build: an entry for a tool the node does not attach, an unknown tool, a tool with no config surface, or a value outside the schema all fail the build — a dead config cannot silently do nothing. Configs never carry secret material; where a tool needs credentials it names a secret by reference (secret_ref).

Today, configs are written in the graph JSON itself (the dashboard's schema-driven dialogs are follow-up work; the subagent picker's gear dialog is the one exception). Existing values round-trip through the canvas untouched.

Metering

Every successful tool call meters into the usage ledger with a unit — web.search by credits (per the depth table above), url.fetch and the connector/subagent bridges by calls, code.exec by seconds. Failed dispatches meter zero. What a unit costs comes from the deployment's rate card ([tools."web.search"] per_credit_usd = …); absent entries are free, never blocked. Usage grouped by tool is on the operator usage route (GET /v1/usage?group_by=tool) and flows into invoices as web.search · 41 credits lines.

Those are metering and deployment-configuration rules, not an Inlay price list. Confirm commercial terms during onboarding.

The redaction contract

Server tool calls appear in your timeline as tool-call / tool-result row pairs with requiresClientResponse: false. By default they are redacted: the real tool name is visible, but arguments arrive as "{}" and the result content is empty — so a private lookup never leaks its query into your client's log, and your UI can still show that the agent searched. (The full args and outcomes belong to the operator trace surface, not the public stream. See Observability for access and capture limits.)

mcp.* and agent.* attachments can opt into full-fidelity rows per node with wire_verbosity: "full" — for connectors you trust with your users' eyes.

The egress guard

Anything that fetches a URL — url.fetch, MCP connectors — goes through one guard: http(s) only; loopback, link-local (including the cloud metadata address), and other non-public ranges blocked at the DNS layer so rebinding tricks don't work; redirects re-validated per hop; response bodies capped. MCP connectors to private network addresses are an explicit operator opt-in per connector; url.fetch never lifts the floor.

Next

MCP connectors — connect tools from compatible Streamable HTTP servers.

On this page