Agentic-mode ask (SSE)

Starts or continues an agentic-mode turn and streams it over the versioned SSE contract

Recent Requests
Log in to see full request history
TimeStatusUser Agent
Retrieving recent requests…
LoadingLoading…

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:

  1. The ids for this turn (init), which every other call in this section is keyed on.
  2. The agent's reasoning as it goes (thinking), and its answer as it is written (answer).
  3. Every tool it invoked and what came back (tool_call / tool_result) — including the SQL it ran and the rows it got.
  4. Every file it wrote, named in a create_artifact tool_result — and an artifact frame for any the user asked to keep.
  5. A checkpoint whenever it needs you (user_question, agent_form, plan_review).
  6. 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

init is the only frame that carries them, and you need both:

IdWhat it is for
threadResponseIdThis turn. Keys answering a clarification, cancelling, polling status and reading the result.
threadIdThe conversation. Pass it back on the next turn to keep context — see Conversation Context.

Request Parameters

ParameterTypeAccepted valuesDefaultDescription
projectIdnumberrequiredThe project to run against. Must be an agentic project.
questionstringrequiredWhat you are asking the agent to do.
threadIdnumbernew threadContinue an existing thread. Omit to open a new one.
languagestringproject languageResponse language.
agentModelstringgpt-5.5 · gpt-5.4 · gpt-5.2gpt-5.4Which model runs the turn. See Model and reasoning effort.
reasoningEffortstringlow · medium · high · xhighmediumHow much the agent deliberates before acting.
rolestringviewer · contributorviewerviewer withholds the tools that mutate your model, knowledge, and skills. See Roles.
planModebooleantrue · falsefalseThe agent drafts a plan and waits for your approval before executing. Emits plan_review.
memoryNamespacestring^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$memory offTurns on persistent memory for this scope — see Memory. Omit it and the agent is not given the memory tools at all.
filesarraynoneAttachments 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.

ModelNotes
gpt-5.5Most capable. Slowest and most expensive.
gpt-5.4Default. The balance most turns want.
gpt-5.2Fastest and cheapest of the three.
EffortNotes
lowLeast deliberation. Good for narrow, well-specified questions.
mediumDefault.
highMore planning before acting. Worth it on multi-step analysis.
xhighMost 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

RoleThe agent can…
viewerDefault. Read your data and answer. Tools that mutate the model, knowledge, or skills are withheld.
contributorAlso 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:

HeaderDescription
Idempotency-KeyA replayed request returns the original turn instead of starting — and billing — a second one.
X-Wren-Session-PropertiesBinds 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.

EventMeaning
initFirst frame. Carries the turn's ids.
thinkingThe agent's reasoning, streamed.
thinking_doneThe reasoning block closed.
answerAnswer text, streamed in pieces keyed by block_id.
tool_callA tool the agent invoked.
tool_resultWhat that tool returned.
user_questionThe agent needs an answer before it can continue.
agent_formThe agent is asking several questions at once.
plan_reviewThe agent is asking you to approve a plan.
subagent_startA delegated sub-task opened.
subagent_thinkingA sub-task's reasoning.
subagent_sql_querySQL a sub-task ran.
subagent_sql_resultWhat that SQL returned.
subagent_endThe sub-task closed, with its findings.
artifactA file was promoted into the project library.
errorThe turn failed.
doneTerminal frame.

init

  • Purpose: Always the first frame. Carries the ids every other call is keyed on.
{
  "threadId": 5,
  "threadResponseId": 7,
  "version": 1
}
FieldTypeDescription
threadIdnumberPass back as threadId to continue this conversation.
threadResponseIdnumberThis turn's id — the path parameter for user_input, cancellation, status and result.
versionnumberSSE contract version.

thinking

  • Purpose: The agent's reasoning, streamed in pieces. Pieces sharing a block_id belong 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 a block_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. sql is present when the tool executes SQL; otherwise the tool's arguments arrive in input.
{
  "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"
}
FieldTypeDescription
block_idnumberOrdering key for this block within the turn.
idstringTool-call id. Pairs this call with its tool_result.
namestringTool name, e.g. execute_sql, render_chart.
sqlstring | undefinedThe SQL being run, when the tool executes SQL.
inputobject | undefinedThe tool's arguments, when it is not a SQL tool.

tool_result

  • Purpose: What the tool returned, keyed to its tool_call by id. 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
}
FieldTypeDescription
idstringThe tool_call.id this result belongs to.
namestringTool name.
sqlstring | undefinedEchoed from the originating tool_call, for SQL tools.
columns / rows / rowCountarray / array / numberPresent for SQL results. rows is truncated to the first 100rowCount is the true total.
chart_specobject | undefinedAn Apache ECharts option, present when the tool drew a chart.
captionstring | undefinedThe chart's caption, when there is one.
outputstring | undefinedFree-form output for tools that return neither rows nor a chart.
errorstring | undefinedPresent 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 optional title, the questions, and allow_skip_all. Answered the same way as user_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 id is the subagent_start.id, not a block_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) and rowCount on success, or error on 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_spec and caption.
{
  "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_artifact writes stays in the thread workspace, addressed by filename, and is announced only by its tool_result. If you wait for artifact frames 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"
}
FieldTypeDescription
artifactIdnumberPass to Get a presigned URL to fetch it.
kindstringWhat it is, e.g. chart, document.
filenamestringThe stored filename.
namestringDisplay name.
contentTypestringMIME type of the stored file.

error

  • Purpose: The turn failed. A done frame 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
}
FieldTypeDescription
session_idstring | nullThe agent session behind this turn.
trace_idstring | nullTrace id for the turn. null when tracing is disabled.
versionnumberSSE contract version.
usageobject | undefinedAggregate 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 threadId returns 409. Wait for done before 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."
}
FieldRequiredDescription
projectIdYesThe project that owns the turn.
questionIdYesThe question_id from the user_question event, verbatim.
answersYesThe selected options.
freeTextNoAnything 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_id from the most recent user_question event.

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 producedArtifacts — 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 sessionMemory — 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 analyseUploads — stage the bytes, then pass the returned references as files
Teach the agent a procedure your team runs oftenSkills — install a SKILL.md into the project
Run this from a serverless function or webhook that can't hold an SSE connection openFire the request, keep threadResponseId, then poll the status and read the result
Re-render an entire conversation after the user reloads your pageList thread messages
Scope a turn to one end user's rows in an embedded, multi-tenant appSend 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-Properties on 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 with 400. 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.

StatusWhen
400Missing 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.
401Missing or invalid API key (UNAUTHORIZED_API_KEY).
404The project or threadId does not exist.
409A 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.
429Out of credits, or a plan limit was hit.
500Upstream 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 INTERRUPTED rather than quietly running to completion. Confirm with Get a turn's status.

🔗

Learn more


Body Params
integer
required
string
required
integer

Continue an existing agentic-mode thread. Omit to start a new one.

string

Response language. Defaults to the project's configured language when omitted.

boolean
Defaults to false

Agent drafts a plan and waits for approval before executing. Emits a plan_review event.

files
array of objects

Attachments for the question, as returned by POST /projects/{project_id}/uploads. Pass filePath verbatim. All entries must come from one upload request — a files array spanning two upload sessions is rejected with 400, because only one session is moved into the thread and the rest would be silently dropped.

files
string
enum
Defaults to gpt-5.4

Model to run the turn on. An id outside this list — including a self-hosted one — falls back to the default rather than being rejected, so check the value you send.

Allowed:
string
enum
Defaults to medium

How much the agent deliberates before acting. Higher settings trade latency and cost for depth on multi-step work. An unrecognized value falls back to the default.

Allowed:
string
enum
Defaults to viewer

Capability role for the turn. viewer (default, safe) hides model/knowledge/skill mutation tools; contributor allows them. Applied at thread provision. Does not affect memory — see memoryNamespace.

Allowed:
string

Opaque caller-chosen name for a persistent memory scope, typically your own end-user id. Memory saved during a turn is recalled on later turns with the same namespace, and its contents are injected into the agent's system prompt at the start of the turn.

Omit it and memory is off — the agent is not given the memory tools at all. That is the right default for stateless automation.

Namespaces are isolated from each other and from the workspace's own users; they can never address a real user's memory. Any role may write memory. Must match ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$ — a single flat token, so / is not accepted; anything else is rejected with 400 before the turn starts. Inspect or erase a namespace with the /v2/projects/{project_id}/memories/{namespace} endpoints.

Headers
string

Comma-separated key=value pairs applied as row/column-level security session properties (e.g. region=US,tier=pro), binding this turn's queries to one of your end users' permissions.

Send it when you open the thread. The bindings are then fixed for the thread's life: a follow-up turn may omit the header and inherits them, or repeat them identically, but one that sends different bindings is refused with 400 — an end user's row visibility cannot change mid-conversation. Bindings also cannot be added to a thread opened without them.

Unlike the ask endpoints, invalid input here fails the request: an unknown or type-mismatched key returns 400 and no turn starts, rather than being echoed back in invalidSessionProperties. Silently dropping a binding could return rows the end user should never see.

string

Opaque key making the request safe to retry. A repeat request with the same key returns the original turn as a JSON body ({ threadId, threadResponseId, idempotent: true }) instead of starting a new turn or charging again.
The key is claimed before any side effect, so a retry that arrives while the original is still opening its turn — provisioning a sandbox takes seconds — gets 409 rather than starting a second turn. Retry again to receive the replay. A request that fails before producing a turn releases the key, so it stays reusable.
Process-local in v1.

string
enum
Defaults to application/json

Generated from available response content types

Allowed:
Responses

Language
Credentials
Bearer
LoadingLoading…
Response
Click Try It! to start a request and see the response here! Or choose an example:
text/event-stream
application/json