The payoff of `save_user_query`: pass a saved row's `queries` array from
`list_saved_queries` and each stored query runs as-is, returning fresh rows
synchronously. Use this INSTEAD of `explore_data_catalogue` whenever a saved row
already covers the question — same SQL, current data, seconds instead of minutes,
and it does not draw on the 20/hour exploration budget.
A full signed URL is accepted in `query_tokens` and unwrapped. For a token
that came from a `get_company_event_web` node rather than a saved row, use
`get_signed_sql_drilldown` — same endpoint, single node, drilldown framing.
`values` slots are substituted deterministically, with no language model in the
loop (~0.5-1.2 s per query). Reach for it whenever you already know the entities —
you usually do, having resolved the tickers or CIKs before calling — and keep
`prompt` for what you cannot type.
- Entity-set slots (type `cik_rows` / `symbol_rows`) take a LIST of {"id",
"name"} objects: [{"id": "0001454027", "name": "Verition Fund Management LLC"}].
A name-only entry ({"id": "", "name": "Walleye Capital"}) is resolved
server-side against the filer registry; a name it cannot resolve comes back as
a 422, never as a guess.
- Pass the SAME dict to every token of a row: each twin picks out the slots it
uses, and keys no twin uses come back as `ignored_values`.
- A slot left out falls back to `prompt`, and then to the value the row was
saved with (reported as `defaulted_parameters`).
`prompt`: a plain `queries` token has no slots and replays unchanged. The backend
maps the wording onto the row's typed slots. A 422 means it could not — fall back
to `explore_data_catalogue` for that question. A prompt-only fill can also DRIFT (a
slot the wording never mentions, a date say, can come back re-aimed), which is the
second reason to prefer `values`: never infer the substituted values from the
wording, read them out of `parameters` and say them back to the user.
`max_rows`: uncapped, a saved row's
`_constituents` companion query returns 7,000-35,000 rows (3-8 MB) and the client
stalls for minutes. A sliced result carries `row_count` (the full count) and
`truncated: true`; say the user is seeing a slice of N rows.
Returns {"queries_run": N, "results": [{"index": i, "format": "table",
"count": <rows returned>, "rows": [...], ...}, ...]}, in the SAME ORDER as
`query_tokens`. A re-aimed replay adds, per result:
`parameters` the values actually substituted (resolved ids and
canonical names)
`defaulted_parameters` slots that kept the saved example ("prior quarter kept
as 2026-03-31")
`ignored_values` keys in `values` this twin does not use
Surface all three: which values were substituted, which kept the saved example,
which were ignored. Rows only — the underlying SQL is never returned, by design.
Describe the results from the row's `metadata.summary`, the substituted
parameters, and the columns, never by asserting what the query does internally.
CITE WHAT YOU PUBLISH: each entry in `results` may carry `_source_*` columns —
`_source_url`, `_source_filed_at`, `_source_document_id`, `_source_citations` —
alongside a `sources` block naming each relation, its grain, and whether it is
`traceable` per "row" or only as a "cohort". That is the provenance of the numbers,
not padding: quote `_source_url` when the user asks where a figure came from, and for anything
you are about to publish — or the moment they ask for the filing, the quote, or
"how do you know" — pass `sources.relations[].relation` plus each row's
identifying columns (the grain AND its date) to `trace_data_sources`, which
resolves them to the source document, the
section, or the verified quote behind the number. `get_document_section` then opens
the exact speaker turn a transcript citation points at.
Each token runs independently: one failure does not stop the rest, and that entry
carries an "error" key instead of rows. A 403 means that token is invalid or
tampered — do not retry it altered; the saved row is dead, so re-run
`explore_data_catalogue`, `save_user_query` a fresh row, and `delete_saved_query`
the old one. A 422 on a re-aimed replay is the mapping failure above (unknown
entity, wrong shape), not a dead row.
A replay returning DIFFERENT rows than when it was saved is normal — it hits live
data. Report what comes back now; never reconcile it against remembered numbers.
Requires auth (your MCP client attaches the OAuth bearer automatically).