Docs

Search

Already know the shape of the answer — typed filters, an aggregation, a meaning search? The routed POST /search/query endpoint 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

bash
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

FieldDefaultWhat it does
query(required)The ask, in plain language.
modeinternalinternal = warehouse only. blended = also fan out web + news for citations.
limit50Internal row cap. Max 1000.
external_limit10Blended citation cap. Max 24 — the fast lane returns a handful of high-signal citations, not a research corpus.
synthesizefalseOpt 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 your interactions warehouse 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 conflated WHERE clause. See Multi-intent asks.
  • blended does everything internal does and fans out a quick web + news citation 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 with external_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 the external_search:execute scope; without it the call quietly degrades to internal-only and sets external_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.

json
{
  "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.status is the warehouse leg's outcome:
    • ok — SQL ran and returned rows (in internal.rows).
    • empty — SQL ran, zero rows matched. The ask is answerable; nothing fit. internal.note explains, and internal.sql shows what ran.
    • unsupported — the ask does not fit a single SELECT over the interactions surface, so no SQL was written. escalate_to_chat is true.
    • failed — SQL was written but execution failed after one self-repair round (or the phase ran out of time). internal.note is a plain-language explanation — never the raw database error.
  • groups is the fanned-out result — one entry per planned sub-question, each with a label, the question it answered, and its own internal block (same shape as the top-level internal). A single-intent ask has one group whose internal is the same object as the top-level internal; render groups when you want the per-question breakdown, or keep reading the flat internal for the primary result. See Multi-intent asks.
  • internal is the PRIMARY group's warehouse result — always groups[0].internal. It is retained as a flat field so existing callers keep working without reading groups.
  • internal.sql is the SQL that ran (or the last attempt) — the receipt behind internal.rows. Show the rows as the snapshot; for a deterministic re-run of the same slice, express it as typed filters via POST /search/query (raw SQL is not directly executable on the public surface).
  • internal.truncated is true when the row set was capped at limit; cached is true when the 1-hour cache served it; repaired is true when the self-repair round rewrote the SQL.
  • external is present only on the blended path when the external leg ran (null otherwise). Any source that missed its deadline is listed by id in external.sources_timed_out — a slow source loses a citation, never the whole call. external.external_query_used shows the enriched query the web pass actually ran.
  • external_omitted is "missing_scope" when you asked for blended but lack external_search:execute, or "not_relevant" when the ask reads as purely a workspace-data question that public web search cannot help answer; null otherwise.
  • synthesis is the opt-in one-paragraph headline (only when you passed synthesize: true and it succeeded; null otherwise).
  • escalate_to_chat is true when the ask is out of the fast lane's reach and Chat is the better door — any sub-question came back unsupported/failed, or the planner spotted an intent the warehouse cannot answer.
  • escalate_reason is 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). null when there is none. It is additive: escalate_to_chat can be true with escalate_reason null (e.g. a failed query), and the data groups it could answer still come back either way.
  • coverage is how current your warehouse data is — latest_event_at (the newest call/meeting/email), latest_ingest_at (the newest sync), total_rows, days_behind, and is_stale. Render it as a freshness badge. When your data is materially behind "now" and a recent-window ask came back empty, the message also 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. null only 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:

  1. 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.lane is "template" and internal.template_id names it.
  2. 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.lane is "semantic".
  3. Free-form lane — anything the first two do not cover falls through to the NL→SQL writer (internal.lane is "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 honest failed.

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:

json
{
  "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:

json
{ "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.