Starts or continues an agentic-mode turn and streams it over the versioned SSE contract
| Time | Status | User Agent | |
|---|---|---|---|
Retrieving recent requests… | |||
The POST /v2/stream/agent_ask endpoint runs one agent turn against your project and streams it back over Server-Sent Events.
Unlike the text-to-SQL endpoints, you don't tell the agent how to answer. It plans, queries your data, runs SQL, draws charts, writes files, and can stop mid-turn to ask you a clarifying question. The stream is how you watch it work.
Project availability: ⛔ Classic projects • ✅ Agentic projects only.
Calling this against a classic project fails.
What It Does
The endpoint returns a Server-Sent Events (SSE) stream containing:
- The ids for this turn (
init), which every other call in this section is keyed on. - The agent's reasoning as it goes (
thinking), and its answer as it is written (answer). - Every tool it invoked and what came back (
tool_call/tool_result) — including the SQL it ran and the rows it got. - Every file it wrote, named in a
create_artifacttool_result— and anartifactframe for any the user asked to keep. - A checkpoint whenever it needs you (
user_question,agent_form,plan_review). - A terminal frame (
done) carrying the session and trace ids.
Basic Usage
Send a project id and a question. Omit threadId to start a new conversation.
{
"projectId": 1,
"question": "Which 5 states have the most customers?"
}The response is text/event-stream. Each frame is an event name and a JSON payload:
event: <type>
data: {json}
A short turn looks like this. The first frame is always init and the last is always done.
// Ids for this turn — capture both before anything else.
event: init
data: {"threadId":5,"threadResponseId":7,"version":1}
// The agent reasons about the question.
event: thinking
data: {"block_id":1,"content":"The customers table has a "}
event: thinking
data: {"block_id":1,"content":"customer_state column, so I can group on it."}
event: thinking_done
data: {}
// It runs SQL to answer it.
event: tool_call
data: {"block_id":2,"id":"toolu_01A","name":"execute_sql","sql":"SELECT customer_state, COUNT(*) AS customers FROM olist_customers_dataset GROUP BY customer_state ORDER BY customers DESC LIMIT 5"}
event: tool_result
data: {"block_id":3,"id":"toolu_01A","name":"execute_sql","columns":["customer_state","customers"],"rows":[["SP",41746],["RJ",12852],["MG",11635],["RS",5466],["PR",5045]],"rowCount":5}
// It writes the answer, streamed in pieces that share a block_id.
event: answer
data: {"block_id":4,"content":"São Paulo leads by a wide margin with "}
event: answer
data: {"block_id":4,"content":"41,746 customers, followed by Rio de Janeiro and Minas Gerais."}
// Terminal frame.
event: done
data: {"session_id":"9c537507-9cec-46ed-b877-07bfa6322bed","trace_id":"f218b1f7-4623-4a56-8b66-18d544797b20","version":1}
This example omits several fields for clarity, and a real turn emits many more frames. See Event Types for the full payload of each one.
Capture the ids from init
initinit is the only frame that carries them, and you need both:
| Id | What it is for |
|---|---|
threadResponseId | This turn. Keys answering a clarification, cancelling, polling status and reading the result. |
threadId | The conversation. Pass it back on the next turn to keep context — see Conversation Context. |
Request Parameters
| Parameter | Type | Accepted values | Default | Description |
|---|---|---|---|---|
projectId | number | — | required | The project to run against. Must be an agentic project. |
question | string | — | required | What you are asking the agent to do. |
threadId | number | — | new thread | Continue an existing thread. Omit to open a new one. |
language | string | — | project language | Response language. |
agentModel | string | gpt-5.5 · gpt-5.4 · gpt-5.2 | gpt-5.4 | Which model runs the turn. See Model and reasoning effort. |
reasoningEffort | string | low · medium · high · xhigh | medium | How much the agent deliberates before acting. |
role | string | viewer · contributor | viewer | viewer withholds the tools that mutate your model, knowledge, and skills. See Roles. |
planMode | boolean | true · false | false | The agent drafts a plan and waits for your approval before executing. Emits plan_review. |
memoryNamespace | string | ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$ | memory off | Turns on persistent memory for this scope — see Memory. Omit it and the agent is not given the memory tools at all. |
files | array | — | none | Attachments from Uploads. All entries must come from one upload request. |
Model and reasoning effort
Both default to a balanced setting, so a request that sends neither runs on gpt-5.4 at medium effort.
| Model | Notes |
|---|---|
gpt-5.5 | Most capable. Slowest and most expensive. |
gpt-5.4 | Default. The balance most turns want. |
gpt-5.2 | Fastest and cheapest of the three. |
| Effort | Notes |
|---|---|
low | Least deliberation. Good for narrow, well-specified questions. |
medium | Default. |
high | More planning before acting. Worth it on multi-step analysis. |
xhigh | Most deliberation. Slowest. |
Both fall back rather than fail. A value outside these lists — including a self-hosted model id — is not rejected; the turn quietly runs on the default instead. Nothing in the response tells you a substitution happened, so validate the value on your side before sending it.
Roles
| Role | The agent can… |
|---|---|
viewer | Default. Read your data and answer. Tools that mutate the model, knowledge, or skills are withheld. |
contributor | Also mutate the model, knowledge, and skills. |
owner is not exposed over the API, and — like an unknown model id — an unrecognized role falls back to viewer rather than erroring. The role is applied when the thread is provisioned, and does not affect memory: any role may write it.
Two headers matter:
| Header | Description |
|---|---|
Idempotency-Key | A replayed request returns the original turn instead of starting — and billing — a second one. |
X-Wren-Session-Properties | Binds the thread to one of your end users' row-level permissions, as comma-separated key=value pairs (e.g. region=US,tier=pro). Send it on the turn that opens the thread. |
Turn lifecycle
graph TD init["init — threadId, threadResponseId"] thinking["thinking / thinking_done"] tool["tool_call → tool_result"] subagent["subagent_start → subagent_end"] artifact["artifact"] answer["answer (streamed by block_id)"] ask["user_question / agent_form / plan_review"] user_input["POST .../user_input"] cancel["POST .../cancellation"] error_event["error"] done["done — session_id, trace_id"] %% Normal flow init --> thinking thinking -->|"Needs data"| tool --> thinking thinking -->|"Delegates a sub-task"| subagent --> thinking tool -->|"Wrote a file the user kept"| artifact --> thinking thinking -->|"Ready to respond"| answer --> done %% Human in the loop — the stream stays open thinking -->|"Needs your input"| ask ask -->|"You reply on the side-channel"| user_input --> thinking %% Exits ask -->|"You stop the turn"| cancel --> done tool -->|"Unrecoverable failure"| error_event --> done thinking -->|"Unrecoverable failure"| error_event
Event Types
The contract is versioned. Every init and done frame carries a version field, currently 1.
New event types may be added under the same version, so clients must ignore event types they do not recognize — that is what lets the contract grow without a version bump.
| Event | Meaning |
|---|---|
init | First frame. Carries the turn's ids. |
thinking | The agent's reasoning, streamed. |
thinking_done | The reasoning block closed. |
answer | Answer text, streamed in pieces keyed by block_id. |
tool_call | A tool the agent invoked. |
tool_result | What that tool returned. |
user_question | The agent needs an answer before it can continue. |
agent_form | The agent is asking several questions at once. |
plan_review | The agent is asking you to approve a plan. |
subagent_start | A delegated sub-task opened. |
subagent_thinking | A sub-task's reasoning. |
subagent_sql_query | SQL a sub-task ran. |
subagent_sql_result | What that SQL returned. |
subagent_end | The sub-task closed, with its findings. |
artifact | A file was promoted into the project library. |
error | The turn failed. |
done | Terminal frame. |
init
- Purpose: Always the first frame. Carries the ids every other call is keyed on.
{
"threadId": 5,
"threadResponseId": 7,
"version": 1
}| Field | Type | Description |
|---|---|---|
threadId | number | Pass back as threadId to continue this conversation. |
threadResponseId | number | This turn's id — the path parameter for user_input, cancellation, status and result. |
version | number | SSE contract version. |
thinking
- Purpose: The agent's reasoning, streamed in pieces. Pieces sharing a
block_idbelong to one reasoning block — concatenate them in arrival order.
{
"block_id": 1,
"content": "The customers table has a customer_state column, so I can group on it."
}thinking_done
- Purpose: The reasoning block closed and the agent moved on to writing its answer. The payload is empty.
{}answer
- Purpose: The answer text, streamed in pieces. As with
thinking, concatenate pieces sharing ablock_id. A turn may produce several answer blocks, interleaved with tool calls.
{
"block_id": 4,
"content": "São Paulo leads by a wide margin with 41,746 customers."
}tool_call
- Purpose: The agent invoked a tool.
sqlis present when the tool executes SQL; otherwise the tool's arguments arrive ininput.
{
"block_id": 2,
"id": "toolu_01A",
"name": "execute_sql",
"sql": "SELECT customer_state, COUNT(*) AS customers FROM olist_customers_dataset GROUP BY customer_state ORDER BY customers DESC LIMIT 5"
}| Field | Type | Description |
|---|---|---|
block_id | number | Ordering key for this block within the turn. |
id | string | Tool-call id. Pairs this call with its tool_result. |
name | string | Tool name, e.g. execute_sql, render_chart. |
sql | string | undefined | The SQL being run, when the tool executes SQL. |
input | object | undefined | The tool's arguments, when it is not a SQL tool. |
tool_result
- Purpose: What the tool returned, keyed to its
tool_callbyid. Which fields are present depends on what the tool produced.
{
"block_id": 3,
"id": "toolu_01A",
"name": "execute_sql",
"columns": ["customer_state", "customers"],
"rows": [["SP", 41746], ["RJ", 12852]],
"rowCount": 5
}| Field | Type | Description |
|---|---|---|
id | string | The tool_call.id this result belongs to. |
name | string | Tool name. |
sql | string | undefined | Echoed from the originating tool_call, for SQL tools. |
columns / rows / rowCount | array / array / number | Present for SQL results. rows is truncated to the first 100 — rowCount is the true total. |
chart_spec | object | undefined | An Apache ECharts option, present when the tool drew a chart. |
caption | string | undefined | The chart's caption, when there is one. |
output | string | undefined | Free-form output for tools that return neither rows nor a chart. |
error | string | undefined | Present instead of a result when the tool failed. A failed tool does not end the turn — the agent usually retries or works around it. |
user_question
- Purpose: The agent stopped and needs an answer. The stream stays open — reply on the side-channel and the turn resumes. See Handling Clarifications.
{
"question_id": "q-abc-123",
"question": "Do you want this by order date or by delivery date?",
"options": ["Order date", "Delivery date"]
}Pass question_id back verbatim as questionId on POST .../user_input.
agent_form
- Purpose: Several clarifying questions at once, rendered as one form rather than a back-and-forth. Carries
form_id, an optionaltitle, thequestions, andallow_skip_all. Answered the same way asuser_question.
plan_review
- Purpose: Emitted when you pass
planMode: true. The agent drafted a plan and is waiting for your approval before executing it.
subagent_start
- Purpose: The agent delegated a sub-task. Everything that sub-task emits carries the same
id.
{
"block_id": 5,
"id": "toolu_01B",
"name": "data-analyst",
"prompt": "Break the top-5 states down by product category.",
"description": "Category breakdown"
}subagent_thinking
- Purpose: A sub-task's reasoning, streamed. Here
idis thesubagent_start.id, not ablock_id.
{ "id": "toolu_01B", "content": "I'll join orders to order_items to reach category." }subagent_sql_query
- Purpose: SQL a sub-task ran.
{ "id": "toolu_01B", "sql": "SELECT product_category_name, COUNT(*) FROM ..." }subagent_sql_result
- Purpose: What that SQL returned —
columns,rows(first 100) androwCounton success, orerroron failure.
{
"id": "toolu_01B",
"sql": "SELECT product_category_name, COUNT(*) FROM ...",
"columns": ["product_category_name", "orders"],
"rows": [["cama_mesa_banho", 11115]],
"rowCount": 73
}subagent_end
- Purpose: The sub-task closed. Carries its narrative
result, and — where it produced them —sql_results,thinking,chart_specandcaption.
{
"block_id": 8,
"id": "toolu_01B",
"name": "data-analyst",
"result": "Bed & bath leads in SP; electronics over-index in RJ.",
"sql_results": [
{
"sql": "SELECT product_category_name, COUNT(*) FROM ...",
"columns": ["product_category_name", "orders"],
"rows": [["cama_mesa_banho", 11115]],
"rowCount": 73
}
]
}artifact
- Purpose: A file was promoted into the project library — i.e. the agent called
save_artifact_to_project, which it does when the user asks to keep the file. This frame is the announcement; fetching it is a separate call.
Most files the agent produces do not emit this frame. Everything
create_artifactwrites stays in the thread workspace, addressed by filename, and is announced only by itstool_result. If you wait forartifactframes you will miss almost everything the turn made — read Artifacts for which endpoint to use.
{
"artifactId": 42,
"kind": "chart",
"filename": "customers-by-state.png",
"name": "Customers by state",
"contentType": "image/png"
}| Field | Type | Description |
|---|---|---|
artifactId | number | Pass to Get a presigned URL to fetch it. |
kind | string | What it is, e.g. chart, document. |
filename | string | The stored filename. |
name | string | Display name. |
contentType | string | MIME type of the stored file. |
error
- Purpose: The turn failed. A
doneframe follows and the stream ends.
{ "message": "Insufficient credit balance" }done
- Purpose: Always the last frame.
{
"session_id": "9c537507-9cec-46ed-b877-07bfa6322bed",
"trace_id": "f218b1f7-4623-4a56-8b66-18d544797b20",
"version": 1
}| Field | Type | Description |
|---|---|---|
session_id | string | null | The agent session behind this turn. |
trace_id | string | null | Trace id for the turn. null when tracing is disabled. |
version | number | SSE contract version. |
usage | object | undefined | Aggregate token usage, present when at least one model call was made. |
Conversation Context
Pass the threadId from the init frame back on your next request and the agent keeps everything before it in context — so a follow-up can say "those" and "that state" without repeating the question.
// First turn — no threadId, so a new thread is opened.
{
"projectId": 1,
"question": "Which 5 states have the most customers?"
}
// init frame of that turn
event: init
data: {"threadId":5,"threadResponseId":7,"version":1}
// Follow-up — same threadId, so the agent still knows what "those" refers to.
{
"projectId": 1,
"threadId": 5,
"question": "How many of those customers ordered more than once?"
}
// init frame of the follow-up: same thread, new turn.
event: init
data: {"threadId":5,"threadResponseId":8,"version":1}Without threadId every turn starts from nothing, and the agent can only ask what you meant.
One turn at a time per thread. A second concurrent request against the same
threadIdreturns409. Wait fordonebefore sending the next turn.
To re-render a whole conversation after a page reload, use List thread messages.
Handling Clarifications
The agent asks rather than guesses. Ask it for "revenue by month" when your schema has both an order date and a delivery date, and instead of picking one it stops and checks.
When that happens it emits user_question — or agent_form for several questions at once — and the SSE stream stays open, waiting on you. Nothing more arrives until you answer, so a client that ignores these frames looks like it hung.
1. The stream pauses on a question
event: user_question
data: {"question_id":"q-abc-123","question":"Do you want this by order date or by delivery date?","options":["Order date","Delivery date"]}Render question as the prompt and options as the choices. Both are meant to be shown verbatim — the agent wrote them for this specific ambiguity, so there is nothing to map or translate. Keep question_id; you need it to reply.
A good prompt also accepts free text alongside the options, because the real answer is sometimes neither ("use the invoice date").
2. You answer on the side-channel
Not a new stream — a separate call against the turn you already have open:
{
"projectId": 1,
"questionId": "q-abc-123",
"answers": ["Order date"],
"freeText": "Use the order timestamp, not delivery."
}| Field | Required | Description |
|---|---|---|
projectId | Yes | The project that owns the turn. |
questionId | Yes | The question_id from the user_question event, verbatim. |
answers | Yes | The selected options. |
freeText | No | Anything extra the user typed. |
The call returns {"success": true} immediately. It does not carry the agent's reply.
3. The original stream resumes
The answer reaches the agent on the connection you never closed, and it carries on from where it paused:
event: thinking
data: {"block_id":6,"content":"Order date it is — filtering on order_purchase_timestamp."}Answering an id the agent is not waiting on — unknown, already answered, or belonging to another turn — is rejected. Always use the
question_idfrom the most recentuser_questionevent.
For agent_form, the payload carries form_id, an optional title, and a questions array where each entry has its own id, label, and a kind of single, multi, or input — render it as one form rather than a sequence of prompts. It is answered the same way.
If you would rather stop than answer, cancel the turn.
Advanced Usage
The stream tells you what happened. These endpoints are how you act on it.
| You want to… | Use |
|---|---|
| Show the chart, report or file the agent just produced | Artifacts — read it straight out of the thread workspace by filename; the project library and its presigned URLs are for files the user asked to keep |
| Have the agent remember a user's preferences across turns, so they don't restate them every session | Memory — pass memoryNamespace on the turn, then inspect or erase it here |
| Let a user drop a CSV or spreadsheet into your UI for the agent to analyse | Uploads — stage the bytes, then pass the returned references as files |
| Teach the agent a procedure your team runs often | Skills — install a SKILL.md into the project |
| Run this from a serverless function or webhook that can't hold an SSE connection open | Fire the request, keep threadResponseId, then poll the status and read the result |
| Re-render an entire conversation after the user reloads your page | List thread messages |
| Scope a turn to one end user's rows in an embedded, multi-tenant app | Send X-Wren-Session-Properties when you open the thread — see below |
Embedding for many end users
An API key carries no user, which shapes three things:
- Row-level security is per thread. Send
X-Wren-Session-Propertieson the turn that opens a thread and its bindings are fixed for the life of that thread. A follow-up may omit the header and inherits them; one that sends different bindings is refused with400. Open a new thread to query as someone else. - Artifact listing is project-scoped. Deciding which artifacts a given end user may see is your responsibility.
- Memory is addressed by a namespace you choose. Use your own end-user id as the
memoryNamespace.
Error handling
Failures before the stream opens return a JSON body with an HTTP status. Failures mid-stream arrive as an error frame and the stream ends.
| Status | When |
|---|---|
400 | Missing projectId or question, an unsupported language, a malformed memoryNamespace, session properties that contradict the ones the thread was opened with, or a files array spanning two upload sessions. Also NO_DEPLOYMENT_FOUND — the project has never been deployed. |
401 | Missing or invalid API key (UNAUTHORIZED_API_KEY). |
404 | The project or threadId does not exist. |
409 | A turn is already running on this thread, or a request with the same Idempotency-Key is still opening its turn. Retry once the original has settled and you get the replay. |
429 | Out of credits, or a plan limit was hit. |
500 | Upstream or stream error. Also emitted as an error frame if the stream had already opened. |
If your client disconnects mid-turn, the turn is stopped and recorded as
INTERRUPTEDrather than quietly running to completion. Confirm with Get a turn's status.
