Two layers
Every tool an agent can call — API, MCP, connected app — gets a size guard unconditionally. A response that would not fit the conversation is cut at an item boundary, the full payload is kept aside for a few minutes, and the agent receives a note explaining what it is looking at and how to get the rest. On top of that, looping is opt-in per tool. Setloop_responses on the
tool’s entry in an agent’s tool list and the tool gains a small set of
reserved arguments that let the agent walk every page, fan out over a list of
ids, and project only the fields it needs — inside one tool call.
queryToolResult and fetchToolResult, that
read a stored result without calling upstream again.
The flag lives on the agent’s binding, not on the tool. Two agents can
hold the same tool with looping on for one and off for the other.
The reserved arguments
Looping adds these arguments to the tool’s input schema. They are stripped before the real request is built, so none of them ever reaches your API._fields
_resultId, so the agent can come back for a
field it left out.
Wrong paths don’t fail quietly. A path that resolves to nothing comes back
as null, and a path that matched nothing on any item is reported in
_unmatchedFields together with the items’ real top-level keys and a sample
item. The agent corrects itself in one step instead of publishing twelve empty
rows.
_all_pages
Offered only when the tool’s own schema declares a page-size parameter and a
page-number or offset parameter. Recognised spellings:
The parameters may sit at the top level or one level down in a request body.
There is nothing to configure: detection reads the schema you imported.
Page one is fetched alone, then pages go four at a time, in order; the walk
stops at the first short page (pages requested past it in the same batch are
discarded, and still count against the request budget), and on a bounded
budget:
A walk that hits a bound says so. The result carries
_pagingComplete: false, _likelyMoreRows: true and a note naming the reason, so the agent
reports a partial set as partial rather than passing it off as complete.
Totals, counts and averages are only presented as sound when every page was
fetched.
_for_each
_for_each and _all_pages combine — every page of
every value — under one shared budget.
The key must be a parameter of this tool. Naming one that isn’t returns an
invalid_for_each_key error that lists the tool’s real parameters. Per-value
failures are reported in _forEachErrors alongside the rows that succeeded;
only if every value fails does the call fail.
_filter
eq, neq, gt, gte, lt, lte, contains, in, exists.
Clauses are ANDed. _filter applies to the merged result of the current call
— after the walk, after the fan-out — so the model never sees the rows it did
not ask for. It is honoured but not advertised to the model on the source
tool, because next to your API’s real filter parameters a model will reach for
the reserved one and fetch everything to filter it afterwards. Use your API’s
own filters first; _filter is for what they cannot express.
To filter rows that were already fetched, with no upstream call at all, the
agent uses queryToolResult on the stored _resultId — see below.
Stored results
When a response is cut, or a walk completes, the full payload is kept for about 15 minutes, scoped to the organization and the thread. The agent receives_resultId and two tools:
queryToolResult—fields, an optionalfilter,offsetandlimitover the stored items. Filtering runs on the full set before paging, so a clause may name a field that was not projected.fetchToolResult— raw character slices for results that have no usable item boundary.
Caching
A complete walk can be cached so that the next identical call — same tool, same arguments, same fan-out values — is served without touching upstream. Cache entries are scoped by whose credentials fetched them: organization-level provider credentials share across the org, per-user runtime credentials are private to that user (or to a tenant-widecache_scope your backend asserts
at mint time). Defaults are 24 hours for org credentials and 1 hour for
runtime credentials, with a per-binding override, dataset_ttl_hours.
A cache hit is labelled: _cache: { hit: true, cachedAt, expiresAt } plus a
note telling the agent that the user can get the very latest data by
re-calling with _refresh: true. Re-calling with different _fields or
_filter is still a hit — the stored set is unprojected and unfiltered.
Caching is enabled per deployment; in shadow mode it records walks without
serving them. Experiment runs never read from the cache.
Live progress
During a long walk the stream carriesdata-tool-loop-progress chunks, at
most one every 750 ms, with the page and request counters (page,
pagesMax, items, requests, requestsMax, elapsedMs) and, for a
fan-out, the value currently being fetched. The same chunks flow through
streamed workflow runs. Use them to render a progress line under the tool
call; nothing in them is needed for correctness.
Tool discovery
A model chooses worse from a long list, and every tool schema it does not need still costs context on every step. Tool discovery keeps the visible list short: part of the agent’s roster is withheld from the prompt and reached through two meta-tools instead.search_toolstakes aqueryand returns the best matches with short descriptions.load_tooltakes atoolName(ortoolNames) and makes those tools callable for the rest of the conversation.
load_tool results in the thread — not from server memory. A
loaded tool survives a restart and a new server instance, and a load that has
scrolled out of the context window is forgotten by the model and the platform
alike.
What is searchable
Tool search is on by default for any agent with more than 40 tools of its own. The per-agent option, saved with the draft and versioned with the rest of the configuration, tunes it or turns it off:
Whatever the configuration says, these stay loaded:
- the rendering components (
showTable,showChart, …) the agent was given, so the model always knows it can render — they never count against the threshold either; queryToolResultandfetchToolResult, so a stored_resultIdcan always be opened;- the workflow builder;
- every tool with
require_approval, because an approved call resumes against the tools the agent holds statically; list_connectionsandconnect_toolkit;- a connected-app entry with
discovery: false.
Connected-app toolkits under the threshold
An agent under its threshold — or with search turned off — still gets connected-app toolkits pooled automatically, so a small agent never carries more schemas than it does today. Above a budget of 15 directly injected tools, toolkits are kept direct whole, smallest first, while they fit; the first toolkit that would not fit moves entirely behind search. That is how a three-tool WhatsApp toolkit stays directly callable next to a sixty-tool Gmail one. Override per entry withdiscovery: true (always pooled) or discovery: false (always direct, and counted
against the budget). Your own API and MCP tools are never pooled by this
budget rule; only the threshold above puts them behind search.
What the agent sees
The agent is told upfront that part of its roster is behind search, which integrations are pooled, and to search before concluding that it cannot do something. Tools the conversation loads are the agent’s own bindings — the size guard, looping arguments, pinned configuration and approval pauses apply exactly as if the tool had been in the prompt from the start. Tools supplied per request with the caller’s own credentials (see Tool auth) are never behind search: they stay directly callable, and the matching catalogue entry is hidden for that request so the caller’s credentials are the ones that apply.search_tools is a keyword search over tool names and descriptions. The
model finds what your descriptions say, so a catalogue imported with good
summaries searches well and one with bare operation ids does not. Fix the
description on the tool, not in the agent’s prompt.In workflows
Atool step takes the same capabilities as a loop block, declared once
rather than chosen per call:
cache, "refresh": true makes the step ignore a stored set, walk
upstream and store the fresh walk for the next reader: what a dashboard
refresh sets on its sources, so the refresh stays live while the views that
reuse the same walk read it back within the TTL. Pages are fetched one, then
four at a time, so a long walk takes about a quarter of the sequential time.
The step returns the tool’s own payload shape with the verdict under a single
_loop key — complete, requests, itemCount, stoppedBy, note — so a
branch can gate on $.steps.fetch._loop.complete. There is no _resultId
in workflows: nothing downstream of a step could redeem one, so the walk’s
byte budget is the only limit. See Building a workflow.
Guidance the agent already has
The agent learns these rules where it meets them. Each looping tool’s own schema describes_fields, _all_pages and _for_each before the first
call, and every trimmed or paged result explains itself in its _note or
_pagingNote: what was cut, whether the set is complete, and how to get the
rest. On top of that, an agent with guarded tools carries a short block in its
system prompt: read those notes, state totals only from a complete set, fetch
the scope that was asked with the API’s own parameters, and describe coverage
to the user in their terms rather than in parameter names.
You do not need to repeat any of it in your own prompt. What is worth writing
is which collections matter: “orders are paginated; always fetch every page
before summarising” is a better instruction than an explanation of
_all_pages.