Search
Already know the shape of the answer — typed filters, an aggregation, a meaning search? The routed
POST /search/queryendpoint is the deterministic sibling of this lane.
Consumer-facing guide to POST /search — the fast lane: one blocking call that returns data now, no agent run. It is the low-latency counterpart to Chat. Where Chat opens a conversation and runs a multi-step Master agent turn server-side (always async, handles only), Search blocks for a single answer: it turns your ask into SQL over your interactions warehouse and runs it, and in blended mode adds a quick web + news citation pass.
Search or Chat?
Reach for Search (POST /search) when… | Reach for Chat (POST /chat) when… |
|---|---|
| You want rows / a count back in one call, right now. | The question needs several steps, judgement, or a written narrative. |
| The ask is a single question over your interaction data. | The ask spans surfaces, needs delegation, or long-form synthesis. |
| A few web + news citations alongside the data is enough. | You want the full market fan-out + the internal-vs-market divergence map. |
| You are wiring a dashboard cell, a slash command, an autocomplete. | You are running an investigation Amdahl should carry out end to end. |
Search settles in seconds (a ~15s hard ceiling). Chat can take minutes. If an ask is too open-ended for the fast lane, Search says so — the response carries escalate_to_chat: true and a plain-language message.
Call it
curl -X POST https://app.amdahl.co/api/platform/v1/search \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "objection-tagged calls in the last 30 days",
"mode": "internal"
}'Parameters
| Field | Default | What it does |
|---|---|---|
query | (required) | The ask, in plain language. |
mode | internal | internal = warehouse only. blended = also fan out web + news for citations. |
limit | 50 | Internal row cap. Max 1000. |
external_limit | 10 | Blended citation cap. Max 24 — the fast lane returns a handful of high-signal citations, not a research corpus. |
synthesize | false | Opt into a one-paragraph headline over the merged evidence. Off by default: the fast lane returns data, not prose, unless asked. |
Unknown fields are stripped; a bad mode / limit surfaces as one invalid_argument error rather than a silent clamp gone wrong.
internal vs blended
internal(default) plans your ask into one or more sub-questions, writes SQL over yourinteractionswarehouse for each, and runs them in parallel through the same query gate the rest of the platform uses — your tenant scope, your data-access policy, the truncation guard, and the 1-hour query cache all apply, and one self-repair round re-writes the SQL if the first attempt fails. A single-intent ask is one sub-question (one group); a multi-intent ask (e.g. "the objections we hit from 11x and how deals with Acme are going") is split so each independent question gets its own well-formed query instead of one conflatedWHEREclause. See Multi-intent asks.blendeddoes everythinginternaldoes and fans out a quickweb+newscitation pass under a tight deadline. The web query is not your raw ask verbatim: a cached, tenant-aware enrichment rewrites it with your company/industry context first (so "enterprise deals" means your pipeline segment, not car-rental coupons), and if the ask reads as purely a question about your own workspace data the external pass is skipped withexternal_omitted: "not_relevant"instead of returning off-topic links. There is deliberately no rerank and no fusion here — the blended pass is built into the fast lane as a quick citation layer; the heavier market synthesis (and the internal-vs-market divergence map) belongs to Chat. Blended needs theexternal_search:executescope; without it the call quietly degrades to internal-only and setsexternal_omitted: "missing_scope"— it never fails for the missing scope.
The result shape
Search never hands back a raw error or stack. Past parameter validation it always returns success: true; every failure mode is a typed field on the result, and a partial answer always beats a 500.
{
"success": true,
"query": "objection-tagged calls in the last 30 days",
"mode": "blended",
"internal": {
"status": "ok",
"sql": "SELECT objection, COUNT(*) AS n FROM interactions WHERE …",
"explanation": "Counts objection-tagged interactions in the last 30 days.",
"rows": [{ "objection": "pricing", "n": 42 }],
"row_count": 1,
"truncated": false,
"cached": false,
"repaired": false,
"note": null
},
"groups": [
{
"label": "Objections",
"question": "objection-tagged calls in the last 30 days",
"internal": {
"status": "ok",
"sql": "SELECT …",
"rows": [{ "objection": "pricing", "n": 42 }],
"row_count": 1,
"note": null
}
}
],
"external": {
"citations": [{ "title": "…", "url": "https://…", "snippet": "…", "source": "web" }],
"sources_timed_out": []
},
"external_omitted": null,
"synthesis": null,
"message": "Found 1 result over your interactions.",
"coverage": {
"latest_event_at": "2026-05-19T05:01:50Z",
"latest_ingest_at": "2026-05-19T05:01:50Z",
"total_rows": 67538,
"days_behind": 1,
"is_stale": false
},
"escalate_to_chat": false,
"escalate_reason": null
}Reading it
internal.statusis the warehouse leg's outcome:ok— SQL ran and returned rows (ininternal.rows).empty— SQL ran, zero rows matched. The ask is answerable; nothing fit.internal.noteexplains, andinternal.sqlshows what ran.unsupported— the ask does not fit a single SELECT over the interactions surface, so no SQL was written.escalate_to_chatistrue.failed— SQL was written but execution failed after one self-repair round (or the phase ran out of time).internal.noteis a plain-language explanation — never the raw database error.
groupsis the fanned-out result — one entry per planned sub-question, each with alabel, thequestionit answered, and its owninternalblock (same shape as the top-levelinternal). A single-intent ask has one group whoseinternalis the same object as the top-levelinternal; rendergroupswhen you want the per-question breakdown, or keep reading the flatinternalfor the primary result. See Multi-intent asks.internalis the PRIMARY group's warehouse result — alwaysgroups[0].internal. It is retained as a flat field so existing callers keep working without readinggroups.internal.sqlis the SQL that ran (or the last attempt) — the receipt behindinternal.rows. Show the rows as the snapshot; for a deterministic re-run of the same slice, express it as typed filters viaPOST /search/query(raw SQL is not directly executable on the public surface).internal.truncatedistruewhen the row set was capped atlimit;cachedistruewhen the 1-hour cache served it;repairedistruewhen the self-repair round rewrote the SQL.externalis present only on theblendedpath when the external leg ran (nullotherwise). Any source that missed its deadline is listed by id inexternal.sources_timed_out— a slow source loses a citation, never the whole call.external.external_query_usedshows the enriched query the web pass actually ran.external_omittedis"missing_scope"when you asked forblendedbut lackexternal_search:execute, or"not_relevant"when the ask reads as purely a workspace-data question that public web search cannot help answer;nullotherwise.synthesisis the opt-in one-paragraph headline (only when you passedsynthesize: trueand it succeeded;nullotherwise).escalate_to_chatistruewhen the ask is out of the fast lane's reach and Chat is the better door — any sub-question came backunsupported/failed, or the planner spotted an intent the warehouse cannot answer.escalate_reasonis a short plain-language reason Chat is the better door — set when the planner detected a non-data intent (advice, a recommendation, a "what should I say" ask, or a source beyond the interactions table).nullwhen there is none. It is additive:escalate_to_chatcan betruewithescalate_reasonnull(e.g. a failed query), and the data groups it could answer still come back either way.coverageis how current your warehouse data is —latest_event_at(the newest call/meeting/email),latest_ingest_at(the newest sync),total_rows,days_behind, andis_stale. Render it as a freshness badge. When your data is materially behind "now" and a recent-window ask came back empty, themessagealso folds in a plain-language note so an empty result reads as a coverage gap ("your data spans through May 19") rather than a mysterious failure.nullonly when coverage could not be resolved (the search still returns).
How the fast lane answers (the lanes)
Every warehouse ask is offered to three tiers in order, so the common questions never pay for an LLM writing SQL under a stopwatch:
- Template lane — a curated, schema-pinned SQL shape for the recurring GTM asks (competitor mentions by account, the rival landscape + win rate, objections, pain points, feature requests, champions, economic buyers). These filter a materialized signal the pipeline already computed (e.g.
sentiment_primary = 'competitive_mention'for competitor mentions — never a raw-text scan), so they are sub-second and correct by construction. When a template answers,internal.laneis"template"andinternal.template_idnames it. - Semantic lane — a theme-shaped ask ("what are customers saying about X", "common themes") is answered from the ML cluster index rather than a row query.
internal.laneis"semantic". - Free-form lane — anything the first two do not cover falls through to the NL→SQL writer (
internal.laneis"freeform"or absent). It is taught the materialized-signal columns so it prefers them over a text scan, its call is bounded + cancellable, and it self-repairs once before giving an honestfailed.
You do not choose a lane — it is picked for you. internal.lane / internal.template_id are there for observability, not required reading.
Multi-intent asks
A single ask can carry more than one independent data question. "The objections we hit from 11x, and how are deals with Acme going?" is really two: one over 11x interactions, one over Acme's. The fast lane plans the ask into the fewest independent sub-questions (capped at 3), writes SQL for each, and runs them in parallel — so each question gets its own well-formed query instead of one conflated company IN ('11x','acme') that returns nothing.
Each sub-question lands as an entry in groups with its own label, question, and internal result. A single-intent ask is one group (byte-identical to before). If part of your ask is not a data question — "…and what should I say to their VP?" — the fast lane does not invent a query for it: it sets escalate_to_chat with an escalate_reason pointing you at Chat, while still returning the data groups it could answer. The whole call still settles under the ~15s ceiling: the sub-questions run concurrently, not one after another.
The only success: false shape is a parameter-validation failure:
{
"success": false,
"error": { "code": "invalid_argument", "message": "Invalid search request: …" }
}Over MCP
The same fast lane is the run action of the search tool on the Amdahl MCP server, with the same parameters:
{ "action": "run", "query": "top objections this quarter", "mode": "blended" }The search tool carries three actions: run (this lane), query (the routed structured/semantic verb), and fields (the filter-field catalog) — the latter two are documented in the Search endpoint guide. Use search when you want data back in one call; use the agents tool's Chat actions (start_chat / chat_status / respond / cancel_chat) when you want Amdahl to run a multi-step investigation server-side. The result is data only — no directive text.
Scopes
data:read is all internal needs, so a read-only key can call it. The blended leg additionally checks external_search:execute and degrades to internal-only when it is absent. Both scopes are on the customer-agent key bundle.