Docs

Chat

Consumer-facing guide to POST /chat — the door for asking Amdahl anything that needs a multi-step, server-side investigation. (Renamed from Search: Chat is the multi-turn agentic lane; the name Search now belongs to the synchronous fast-search endpoint, POST /search — reach for that when you want data back in one blocking call rather than a full agent run.) Every ask opens (or continues) a named Chat and runs one Master agent turn over your workspace data: warehouse queries, customer-voice themes, knowledge base, long-term memory, and (when enabled) external market search.

The lifecycle

Chat has exactly one lifecycle on every surface:

code
START  →  { chat_id, run_id, status, stream_url, read_url, resume_url }
   │
   ├─ SUBSCRIBE (SSE on stream_url)   live events: tools, delegates, content blocks, pauses
   ├─ READ (GET read_url)             snapshot anytime; ?wait_ms= long-polls up to 30s
   └─ ANSWER (POST resume_url)        only when status is awaiting_input

START never blocks for the final answer. There is no stream flag and no synchronous mode — streaming is how you watch a run that already started, and "just give me the answer" is a client-side wait: START, then poll read_url until the run settles. Treat the three returned URLs as opaque handles.

StatusMeaningWhat to do
queued / runningWork in flightSubscribe to stream_url or poll read_url
awaiting_inputPaused on a human questionRender the pause; POST the answer to resume_url
completeFinal answer on the runRead answer.answer_text + answer.content_blocks
error / canceledDeadSurface the error; do not resume

START

bash
curl -X POST https://app.amdahl.co/api/platform/v1/chat \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "What are the top objections in enterprise deals this quarter?",
    "name": "Enterprise objections",
    "config": { "depth": "standard" }
  }'

Response (immediately — the run is queued, not finished):

json
{
  "success": true,
  "chat_id": "3f0e2a1b-…",
  "run_id": "9a8b7c6d-…",
  "status": "queued",
  "stream_url": "/api/platform/v1/conversations/3f0e2a1b-…/turns/9a8b7c6d-…/stream",
  "read_url": "/api/platform/v1/chats/3f0e2a1b-…/runs/9a8b7c6d-…",
  "resume_url": "/api/platform/v1/conversations/3f0e2a1b-…/turns/9a8b7c6d-…/resume"
}

Pass chat_id instead of name to continue an existing Chat — the new turn shares the Chat's memory (its substrate) with every earlier turn.

Body fields

FieldRequiredWhat it does
inputyesThe ask, in plain language.
chat_idnoContinue an existing Chat.
namenoName a NEW Chat. Omitted: auto-titled from the input.
agentnoPin the turn to a named agent (library slug like researcher, or your own workspace agent).
confignoThe knobs below.

Config (the only knobs)

FieldDefaultNotes
depthdeepquick | standard | deep. A real investigation tier — see Depth below.
actions_allowed(absent = all)Outbound actions this run may fire (e.g. email_member). Absent = every cataloged action is allowed (the default); pass a list to narrow, or [] to disable actions entirely.
write_outputsfalseAllow living-doc commits.
write_memoryfalseAllow long-term memory commits.
on_questionautopause (interactive), auto (proceed on judgment), none (headless fail-fast).
external_searchfalseAllow paid market fan-outs (deep forces this on).
include_divergencefalseFuse the internal-vs-market divergence map (deep forces this on).

There is no stream, model, temperature, evidence, or as_of field. The model is picked by the depth tier, not by you. Evidence (citations and the query behind every data-backed block) is always attached — it is not a toggle. As-of / time-travel is not a Chat knob: an interactive Chat always reads your current data; historical "as we understood it on date X" reads belong to workflow backtests. Unknown fields are rejected with a structured invalid_argument error so a typo never silently changes behavior.

Depth

depth is a genuine investigation tier — the platform sets the model, the turn budget, and the toolset for you:

TierModelTurn budgetToolsetReach for it when
quickfast~10lean (fast data reads + fast search)a focused lookup you want back quickly; no fan-out, no delegation.
standarddefault~50fullthe middle tier — the full toolset at the profile's own model.
deepmost capable~75full, and external_search + include_divergence are forced ona hard, multi-part question worth decomposing and cross-checking; the run is told to break the ask into sub-questions and self-verify every figure before answering.

You never pass a model — the tier picks it. The default is deep: an unspecified depth runs the full investigation tier, on the reasoning that a Chat exists to be right, not fast. Pass standard or quick explicitly when latency or cost matters more than depth on a given ask.

Watching a run

Subscribe (SSE) — the same event stream the console renders:

bash
curl -N -H "X-API-Key: $API_KEY" \
  "https://app.amdahl.co$STREAM_URL"

Or poll — wait_ms long-polls up to 30 seconds per request, returning the same body a plain GET returns (never a second START shape):

bash
# repeat until status is complete | awaiting_input | error | canceled
curl -H "X-API-Key: $API_KEY" \
  "https://app.amdahl.co$READ_URL?wait_ms=30000"

The READ body carries status, the pause payload when awaiting input, usage, and the answer envelope:

json
{
  "data": {
    "run": {
      "chat_id": "…",
      "run_id": "…",
      "status": "complete",
      "pending_input": null,
      "answer": {
        "answer_text": "Enterprise objections cluster around…",
        "content_blocks": [
          { "id": "b1", "type": "text", "markdown": "…", "presented_at": "…" },
          {
            "id": "b2",
            "type": "chart_spec",
            "chart_type": "bar",
            "encoding": { "x": "objection", "y": "n" },
            "query": { "kind": "data.query", "surface": "interactions", "sql": "SELECT …" },
            "data": [{ "objection": "pricing", "n": 42 }],
            "presented_at": "…"
          }
        ],
        "follow_ups": [
          "Break the pricing objections down by segment?",
          "How did objections shift since Q1?"
        ]
      },
      "usage": { "tokens": { "input": 51234, "output": 2210 }, "turns_used": 7 }
    }
  }
}

Two more fields ride the READ body:

  • answered_asks — always present. Every ask_a_human exchange that was already answered in this run, as { question, answer, option_id }, rebuilt from the run's own log so a reloaded transcript can replay its Q&A history (an unanswered ask surfaces via pending_input instead).
  • events — opt-in via ?include=events. The persisted activity trace: the run's tool calls (tool_start / tool_complete / tool_error, with the same plain-language labels the live stream carries) plus every sub-agent's tool calls wrapped in the live stream's child_progress / child_terminated envelope shape, merged in timestamp order and capped to the newest 500. Use it to re-render "what the run did" after a reload; leave it off on wait_ms poll loops — the default body stays lean.

Answers are content blocks

Answers are an ordered list of typed content blockstext, callout, citation, table, chart_spec, metric — not one opaque string (answer_text is the flattened markdown for logs and dumb clients). Data-backed blocks (table, chart_spec, query-backed metric) always carry both the declared query (re-run it live via data.query for current numbers) and the snapshot data (exactly what the agent saw when it presented the block), on every surface. That pair is what powers Snapshot / Live / Diff rendering: show the snapshot instantly, re-run the query for live values, diff the two over time.

chart_spec.chart_type is a closed vocabulary the agent picks per data shape: bar (comparisons), line / area (trends), pie / treemap (share-of-total), funnel (stage progressions), radar (multi-dimension profiles), scatter (correlations), and gauge (one value against a target — renders the first data row only: x is the label, the first y is the value, an optional second y is the max, defaulting to 100). All shapes share the same { x, y, series? } encoding over the snapshot rows.

Blocks arrive mid-run as content_block events on the SSE stream and reconcile into the terminal answer.content_blocks in presentation order. Ignore block types you do not recognize — the catalog grows additively.

Suggested follow-ups

answer.follow_ups (always present; empty when none) is a short list — at most 4 — of suggested next questions the agent attached to its final answer block. Each entry is a complete, runnable question grounded in what the answer just found and scoped to what the workspace can actually answer — the agent only suggests asks its own tools (warehouse queries, theme search, knowledge base, web research) or a dispatchable agent can carry out, never something needing an unconnected data source or a disallowed action. A client can render them as one-click chips that send the question as the next turn (the console does exactly that), and an API or MCP consumer can feed one straight back into chat.start / respond. The agent supplies them on every normal answer; a turn that ended in plain streamed prose may come back with an empty list.

Intent enrichment

Before the agent's first turn on a Chat ask (Master lane, standard / deep depth), a bounded enrichment phase interprets the plain query against light tenant context — business profile, the living-doc catalog, the warehouse surface names — and produces an intent brief: the original query verbatim, the interpreted intent, a fully-specified expanded question, and hints at the relevant surfaces. The brief is injected into the model request as advisory guidance (the persisted user message is never modified; the agent is instructed to answer the ORIGINAL question and override the brief where it misreads the ask) and is fail-open: a timeout or error simply skips it. When produced, it surfaces on the run READ as an optional intent_brief field ({ original_query, interpreted_intent, expanded_question, hints[] }) so clients can render a collapsed "How we understood your question" card; absent means the turn ran unenriched.

Linked data phrases

Block and prose markdown may hyperlink a specific figure or phrase — a metric, a theme label, a company, a segment, anything a query backs — to a suggested follow-up using the closed amdahl:q link scheme — a standard markdown link whose destination is amdahl:q?.... For the phrase "pipeline grew $42k", the link destination is:

code
amdahl:q?fu=break%20this%20down%20by%20segment&block=blk_1

fu (required, ≤300 chars decoded) is the follow-up question; block optionally points at a presented content block whose query + snapshot back the phrase; sql (≤2000 chars decoded) is the inline fallback when no block does. Rich clients render the phrase as an interactive chip (hover previews the query, click seeds the composer with fu); everything else can treat it as a plain link or ignore it. The grammar is enforced server-side — malformed amdahl: links are unwrapped to plain text before persisting, and answer_text always arrives with the links stripped to clean prose, so non-rendering consumers never see the scheme.

Query detail on tool events

Query-shaped tool events (live SSE, persisted, and the ?include=events replay) carry an optional queryDetail — the FULL query behind the call (sql capped at 4000 chars, or the natural-language question, plus surface / asOf where relevant) — because the generic inputSummary is truncated at 200 chars. tool_complete on a query op may also carry resultStats (rowCount, truncated, cached). Both fields are additive and absent on non-query tools.

Answering a human question

With on_question: "pause", a run that calls ask_a_human lands in awaiting_input: the READ body's pending_input carries the structured question (multiple choice always includes an Other write-in, or free form). Answer by POSTing the resume body to resume_url — the standard turn resume. The SSE stream stays open across the pause. With the default on_question: "auto", runs never pause — the agent states its assumption and proceeds.

Chats

Chats are the named threads runs ride on. List, inspect, and rename them:

VerbWhat it does
GET /chatsList chats, newest activity first (status, limit, offset).
GET /chats/:idOne chat: snapshot, runs with read handles, active runs, pending human asks (each with its resume_url).
GET /chats/:id/runs/:run_idThe READ endpoint from the START handles (supports ?wait_ms= and ?include=events).
PATCH /chats/:idRename: { "name": "…" }.

Every turn in a Chat shares one substrate — the scratch memory context.remember / context.query_substrate write and read — so follow-up asks build on earlier findings, and any sub-agents dispatched inside a run share it too.

Over MCP

The same lifecycle is on the Amdahl MCP server as four actions on the agents tool, never blocking a full run inside one tool call (MCP tool calls have a hard time ceiling; a deep Chat can take minutes):

ActionRole
start_chatSTART — same body as POST /chat (input, optional chat_id / name / agent / config); returns the handles immediately.
chat_statusREAD — { chat_id, run_id, wait_ms? }; the same snapshot as read_url, wait_ms long-polls up to 30s.
respondANSWER — { run_id, response } where response matches the pause schema chat_status surfaced.
cancel_chatCancel one run — { run_id, reason? }.

The check-in loop: start_chat → poll chat_status (or read the chat://<id>/runs/<run_id> MCP resource) until the run settles → respond if it paused on a question. Chat reads are also MCP resources: chat://list and chat://<id>. Scopes: conversations:write to start, conversations:read to read, workflows:write to respond/cancel — all on the customer-agent key bundle (existing keys were backfilled).

Per-surface defaults

Callerwrite_memorywrite_outputson_questionexternal_search
Bare API / MCP keyfalsefalseautofalse
Console (session JWT)truefalsepausetrue
Routine fireper recipeusually truenoneper recipe

The server default is the bare-API row; the console and routines set their own knobs explicitly on each START.