/ask

Ask in plain English.
Get JSON back.

POST a plain-English question to /v1/tenants/:t/ask and get rows back with the plan that produced them attached. The model picks from plans the cost walker already ranked, and the LLM call runs on your own provider key.

123 top 5 customers by spend in March cost router vector.topk graph.walk est 41ms est 87ms sql.aggregate result · 2 rows customer spend c_84 4,204 c_12 3,977 plan + cost attached
the pipeline

How a question becomes a plan.

The model never free-writes a query. It picks from a shortlist the cost walker already ranked — so the expensive plan it might have guessed was filtered out before it could choose it.

01
You ask

POST a plain-English question to /ask. No query language, no shape selection — the question is the whole request.

"q": "Top 5 customers by spend…"
02
Cost walker shortlists

Candidate plans across SQL, vector, graph and full-text are ranked with real cardinality from oc-stats. Cheap plans rise above expensive ones before any model is consulted.

oc-stats → ranked candidates
03
Model picks

The LLM — running on your key — chooses from the cost-tested shortlist. It cannot select a shape that never passed the cost test.

choice ⊆ shortlist
04
Rows, plan attached

The response carries the rows plus the chosen plan and per-node cost. Nothing about how the answer was produced is hidden.

"plan" · "cost" · "explain"
one round trip

The answer arrives with its receipts.

A single POST carries the question. The response carries the rows — and the plan that produced them, with per-node cost. The question on the right routes to sql.aggregate + sql.semi_join: an aggregate for spend, a semi-join for the login check.

If the plan looks wrong, you don't debug a black box — you pin a shape and re-run. How each underlying mode works is covered in the architecture guide.

/ask
> POST /v1/tenants/:t/ask
> { "q": "Top 5 customers by spend in March
>         who haven't logged in this week" }

{
  "plan": "sql.aggregate + sql.semi_join",
  "cost": { "rows_estimate": 5, "io_cost": 8.2 },
  "rows": [ { "id": "c_84", "spend": 4204.10 }, … ],
  "explain": { "nodes": [ … ] }
}
why it doesn't guess

Three properties that make /ask debuggable.

Cost-informed

The model chooses from plans that already passed a cost test built on real cardinality from oc-stats — not from whatever shape its training data favors.

oc-stats cardinalityranked before the pick
Deterministic

Same question → same plan → same rows, modulo data. Replays are stable. If you log a question today, re-running it tomorrow exercises the same plan.

stable replayssame plan, same rows
Overridable

Every response carries the plan it ran with per-node cost. Disagree with the pick? Pin a shape and re-run — the override is a first-class request, not a workaround.

EXPLAIN in every responsepin + re-run
byok llm

Your key. Your audit. Your bill.

The model call inside /ask runs on a key you bring. We keep the planner and the security boundary; your provider keeps the token bill. No token markup is billed — what your provider charges is what LLM usage costs you.

Read BYOK LLM →
Providers

Bring a key from any of the four supported providers. Switch providers without touching your queries.

OpenAIAnthropicGeminiGroq
Keys at rest

Keys are envelope-encrypted at rest. They are decrypted only when a request needs them.

envelope-encrypted
Audit

The audit trail shows which provider and model ran each call, with prompt and completion token counts.

providermodelprompt + completion tokens
Fallback & billing

A platform key exists as a fallback, but it is opt-in only. Token usage is never marked up — there is no LLM line on your bill.

fallback opt-in onlyno token markup
next

Read the /ask docs, then send your first question.