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.
Pages are fetched sequentially so upstream rate limits are respected. The walk
stops at the first short page, 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
Connected-app toolkits can be large — Gmail alone is more than sixty tools — and a model chooses worse from a long list. Above a budget of 15 directly injected tools, connected-app tools move behind two meta-tools: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.
discovery: true (always pooled) or discovery: false (always direct, and counted against the budget). list_connections and
connect_toolkit are always direct.
Your own API and MCP tools are never pooled. Prune them at the agent instead —
see Connecting your API.
In workflows
Atool step takes the same capabilities as a loop block, declared once
rather than chosen per call:
_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
An agent holding looping tools is given the rules above as a separate system message: walk before you count, project before you read, report a capped walk as partial, and never name the mechanism to the user. 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.