Asks a question and streams SQL generation, execution, and answer events using server-sent events
| Time | Status | User Agent | |
|---|---|---|---|
Retrieving recent requests… | |||
The POST /v2/stream/ask endpoint lets you ask a single question and stream the whole answer over Server-Sent Events (SSE) — SQL generation and execution state, the streamed natural-language summary (or an explanation for non-SQL questions), and mid-stream clarification checkpoints you can answer without restarting the turn.
This is the v2 (adhoc) streaming ask — the successor to the v1 /stream/ask. Two things are intentionally different from v1:
- Charts are not part of this stream. Once you have the generated SQL, render a chart separately with
POST /v2/generate_chart(Apache ECharts). - Clarification is built in. When the AI needs input, the stream emits a
pendingQuestionframe; you answer it withPOST /v2/stream/ask/respondand the same open stream resumes.
What It Does
The endpoint returns a Server-Sent Events (SSE) stream that includes:
- A leading
initframe carrying thethreadId(use it for follow-ups). - Processing state for SQL generation (understanding → searching → planning → generating → correcting) and execution.
- The generated SQL and, for SQL answers, a streamed natural-language
summary_generationcontent block — or, for non-SQL questions, a streamedexplanationcontent block. pendingQuestionframes when the AI pauses for clarification at theintentorsql_reasoningcheckpoint.
Basic Usage
{
"projectId": 1,
"question": "List the top 5 states with the most customers"
}This simplified stream shows the key phases of a /v2/stream/ask request — from the leading init frame, through understanding and generating SQL, executing it, and finally producing a streamed summary.
// Stream begins
data: { "type": "init", "threadId": "0625991d-1bba-407d-8ad4-dd0210172484", "queryId": null }
data: { "type": "message_start" }
// SQL Generation Stages
data: { "type": "state", "data": { "state": "sql_generation_start" }}
data: { "type": "state", "data": { "state": "sql_generation_understanding" }}
data: { "type": "state", "data": { "state": "sql_generation_searching" }}
data: { "type": "state", "data": { "state": "sql_generation_planning" }}
data: { "type": "state", "data": { "state": "sql_generation_generating" }}
data: { "type": "state", "data": { "state": "sql_generation_success", "sql": "SELECT ... LIMIT 5" }}
// SQL Execution
data: { "type": "state", "data": { "state": "sql_execution_start" }}
data: { "type": "state", "data": { "state": "sql_execution_end" }}
// Summary Generation (streamed content block)
data: { "type": "content_block_start", "content_block": { "type": "text", "name": "summary_generation" }}
data: { "type": "content_block_delta", "delta": { "text": "Here" }}
data: { "type": "content_block_delta", "delta": { "text": " are" }}
data: { "type": "content_block_delta", "delta": { "text": " the top 5 states by customer count." }}
data: { "type": "content_block_stop" }
// Stream ends
data: { "type": "message_stop" }
This example omits several detailed properties (such as question, rephrasedQuestion, intentReasoning, traceId, and timestamp) for clarity. Refer to the full schema for a complete breakdown of all available fields.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
projectId | number | ✅ | Target project. |
question | string | ✅ | The natural-language question. |
threadId | string | Continue an existing thread. Supplying it also enables the single-active-turn guard (see below). | |
sampleSize | number | Row limit for the SQL execution preview. | |
language | string | Language used for the summary/explanation (e.g. "English"). Defaults to the project language. | |
returnBothSqlDialect | boolean | When true, sql_generation_success also carries the native dialectSql. Defaults to false. | |
customInstruction | string | Extra instruction passed to the AI for this turn. | |
allowUserGuide | boolean | Set false to stop answering product questions from the built-in user guide. | |
userGuidePrompt | string | Replace the built-in user guide with your own. Ignored when allowUserGuide is false. |
Single-active-turn guard. When you supply a
threadId, a second concurrent/v2/stream/askfor the same thread is rejected with 409 (A turn is already in progress for this thread). OmitthreadIdand the server generates one (returned in theinitframe).
User Guide Controls
Before answering, Wren classifies what a question is asking for. Most questions are about the data and become SQL. Some are not: "Do I need to know SQL?", "How do I get started?" Wren recognises those as product help and answers them from a built-in user guide describing Wren itself.
If you have embedded Wren in your own product that is usually the wrong answer — your users have never heard of Wren, and the guide describes our product rather than yours. Two per-request fields change it:
allowUserGuide(boolean, defaulttrue) — setfalseand product help is removed from classification altogether. The question is classified as something else and the built-in guide is never consulted.userGuidePrompt(string) — supply your own guide and Wren uses it in place of ours, both when classifying the question and when answering it. Ignored whenallowUserGuideisfalse.
Both default to today's behavior, so omitting them leaves the request unchanged. Both are per-request: re-send them on every turn of a thread. See User Guide Controls for the shared behaviour, including what happens to a question your guide does not cover.
Switch the built-in guide off:
{
"projectId": 1,
"question": "How do I get started?",
"allowUserGuide": false
}Or answer from your own guide instead — same question, your content:
{
"projectId": 1,
"question": "How do I get started?",
"userGuidePrompt": "# Northwind Insights - Help\n\n## Getting started\nOpen **Explore** in the left sidebar and pick a dataset. Type your question in\nthe box at the top; results appear as a table you can switch to a chart.\n\n## Saving a report\nClick **Save** in the top-right of any answer to add it to *My Reports*.\nSaved reports refresh every morning at 06:00 UTC.\n\n## Sharing a report\nOpen a saved report and choose **Share > Copy link**. Links work only for\nteammates in your workspace; external sharing must be enabled by an admin."
}The answer streams as an explanation content block.
Frames below are real but abridged — the stream opens with
init,message_startand three SQL-generationstateframes that are not shown, and the token-by-tokentext_deltaframes are collapsed. The complete streamed text follows each block, with the literal\nsequences rendered as line breaks.
data: {"type":"content_block_start","content_block":{"type":"text","name":"explanation"},"timestamp":1788000689141}
data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"To"},"timestamp":1788000689142}
data: {"type":"content_block_delta","delta":{"type":"text_delta","text":" get"},"timestamp":1788000689143}
// ... 153 more text_delta frames ...
data: {"type":"content_block_stop","timestamp":1788000689229}
data: {"type":"message_stop","data":{"threadId":"05cef683-22d8-4cf8-a562-444dc2f804cc","duration":3124},"timestamp":1788000689231}To get started with analyzing your e-commerce data, follow these steps:
- Understand your data schema: Review tables like orders, products, customers, payments, and reviews.
- Define your goal: What insights do you want? For example, customer satisfaction, sales trends, or product performance.
- Explore key tables: Start with orders and reviews to analyze sales and customer feedback.
- Join related tables: Combine orders with payments, products, and sellers for deeper insights.
- Ask specific questions: For example, "Which products have the highest ratings?" or "What is the average delivery time?"
Better questions to ask:
- What are the top-selling product categories?
- How does payment type affect order completion?
- Which regions have the most satisfied customers?
data: {"type":"content_block_start","content_block":{"type":"text","name":"explanation"},"timestamp":1788000701720}
data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"To"},"timestamp":1788000701721}
data: {"type":"content_block_delta","delta":{"type":"text_delta","text":" get"},"timestamp":1788000701722}
// ... 87 more text_delta frames ...
data: {"type":"content_block_stop","timestamp":1788000701771}
data: {"type":"message_stop","data":{"threadId":"3fc4ff3e-9f23-49b6-8a75-9968597a266b","duration":3089},"timestamp":1788000701773}To get started with Northwind Insights, follow these steps:
- Open Explore in the left sidebar.
- Pick a dataset that you want to analyze.
- Type your question in the box at the top.
- View the results, which will appear as a table by default.
- You can switch the table view to a chart if you prefer a visual representation.
This will help you begin exploring your data easily.
This endpoint sends no usage object — its message_stop carries only threadId and duration — so the streamed text is the only signal. The v1 /stream/ask does report usage there.
Clarification (pendingQuestion → respond)
pendingQuestion → respond)At the intent or sql_reasoning checkpoint the AI may need you to disambiguate. Instead of failing, the stream emits a pendingQuestion frame and keeps the connection open:
data: {
"type": "pendingQuestion",
"data": {
"questionId": "b8f0c1e2-...",
"checkpoint": "intent",
"question": "Which \"customers\" did you mean?",
"options": [
{ "value": "all", "label": "All registered customers" },
{ "value": "active", "label": "Only active customers" }
],
"selectionType": "single",
"rationale": "The question is ambiguous about customer status.",
"queryId": "1f0a3c7d-...",
"threadId": "0625991d-1bba-407d-8ad4-dd0210172484"
},
"timestamp": 1751014955000
}Answer it with a separate call to POST /v2/stream/ask/respond, carrying the queryId and questionId from the frame. This call returns immediately ({ "status": "accepted" }); the ask resumes on the same open SSE stream — you do not reopen /v2/stream/ask.
{
"projectId": 1,
"queryId": "1f0a3c7d-...",
"questionId": "b8f0c1e2-...",
"action": "SUBMIT",
"answers": ["active"],
"threadId": "0625991d-1bba-407d-8ad4-dd0210172484"
}action | Meaning |
|---|---|
SUBMIT | Answer with answers (option values) and/or freeText. |
SKIP | Skip the question and let the AI proceed with its best guess. |
RETRY | Ask the AI to regenerate the clarification question. |
CLOSE | Cancel the clarification and end the turn. |
action is case-insensitive. See the stream/ask/respond reference for the full request/response contract and error codes.
State Lifecycle
graph TD init["init"] message_start["message_start"] sql_start["sql_generation_start"] sql_understanding["sql_generation_understanding"] sql_searching["sql_generation_searching"] sql_planning["sql_generation_planning"] sql_generating["sql_generation_generating"] sql_correcting["sql_generation_correcting"] sql_success["sql_generation_success"] sql_failed["sql_generation_failed"] sql_stopped["sql_generation_stopped"] sql_exec_start["sql_execution_start"] sql_exec_end["sql_execution_end"] pending["pendingQuestion (intent / sql_reasoning)"] respond["POST /v2/stream/ask/respond"] summary_start["content_block_start: summary_generation"] summary_streaming["content_block_delta (text)"] summary_stop["content_block_stop"] explanation_block["content_block_*: explanation (non-SQL)"] error_event["type: error"] message_stop["message_stop"] %% Normal flow init --> message_start -->|"Trigger SQL generation"| sql_start sql_start --> sql_understanding --> sql_searching --> sql_planning --> sql_generating sql_generating -->|"Auto-correct (if needed)"| sql_correcting sql_correcting --> sql_success sql_generating --> sql_success sql_success -->|"Run SQL"| sql_exec_start --> sql_exec_end sql_exec_end -->|"Start LLM summary"| summary_start summary_start --> summary_streaming --> summary_stop --> message_stop %% Non-SQL branch sql_understanding -->|"Non-SQL question"| explanation_block --> message_stop %% Clarification checkpoints (resume on the same open stream) sql_understanding -.->|"Intent checkpoint"| pending sql_generating -.->|"SQL-reasoning checkpoint"| pending pending -.->|"Answer via respond"| respond respond -.->|"Turn resumes"| sql_generating %% Error flows sql_generating -->|"Too many corrections failed"| sql_failed -->|"Emit error event"| error_event --> message_stop sql_generating -->|"User canceled"| sql_stopped --> message_stop sql_exec_start -->|"SQL execution error"| error_event --> message_stop summary_start -->|"LLM failed to summarize"| error_event --> message_stop
Event Types
During a /v2/stream/ask request, the API streams a sequence of events using Server-Sent Events (SSE). Each event provides insight into the system’s current state or output.
init
- Purpose: Leading frame of every stream. Delivers the
threadIdto use for follow-ups. - Payload:
queryIdis alwaysnullhere — the per-turnqueryIdused to answer clarifications is delivered with eachpendingQuestionframe instead.
{
"type": "init",
"threadId": "0625991d-1bba-407d-8ad4-dd0210172484",
"queryId": null
}message_start
- Purpose: Indicates the start of a new streaming response.
- Payload: Contains the timestamp when the process began.
{
"type": "message_start",
"timestamp": 1751014954139
}state
- Purpose: Describes what stage the system is in during processing.
State Lifecycle Overview
state | Description |
|---|---|
sql_generation_start | The system has begun processing the user’s question. |
sql_generation_understanding | The AI is interpreting the question and trying to identify its intent. |
sql_generation_searching | The AI is searching relevant tables and metadata to answer the question. |
sql_generation_planning | A plan for how to generate SQL is being formed, including table joins or filters. |
sql_generation_generating | The SQL is being generated. |
sql_generation_correcting | A generated SQL failed to execute and is being corrected. (Auto-retry phase) |
sql_generation_success | SQL was successfully generated. The sql field will be included. |
sql_generation_failed | SQL generation failed. Followed by an error event. |
sql_generation_stopped | SQL generation was manually canceled or interrupted (e.g. client disconnected). |
sql_generation_finished | Final state for SQL generation (used internally; usually followed by success or failed). |
sql_execution_start | SQL execution has started. |
sql_execution_end | SQL execution has completed (successfully or not). |
data Field Reference ("type": "state")
data Field Reference ("type": "state")🔹 sql_generation_start
| Field | Type | Description |
|---|---|---|
state | string | "sql_generation_start" |
question | string | The original user input. |
threadId | string | Unique thread identifier. |
language | string | Language used for summarization (e.g., "English"). |
🔹 SQL Generation In-Progress States
(e.g., sql_generation_understanding, searching, planning, generating, correcting)
| Field | Type | Description |
|---|---|---|
state | string | Current generation state, such as "sql_generation_searching", "sql_generation_planning", etc. |
pollCount | number | Number of polling attempts so far. |
rephrasedQuestion | string | null | Reformulated version of the user query. |
intentReasoning | string | null | AI’s interpretation of what the user is asking. |
sqlGenerationReasoning | string | null | Step-by-step reasoning of SQL generation. Appears during generating. |
retrievedTables | string[] | null | List of tables determined to be relevant. |
invalidSql | string | null | SQL that failed during correction attempts (optional). |
traceId | string | For backend debugging. |
🔹 sql_generation_success
| Field | Type | Description |
|---|---|---|
state | "sql_generation_success" | Marks the completion of SQL generation. |
sql | string | The generated SQL query. |
dialectSql | string | undefined | Native dialect SQL, included when returnBothSqlDialect is true. |
🔹 sql_execution_start
| Field | Type | Description |
|---|---|---|
state | "sql_execution_start" | SQL execution has begun. |
sql | string | The SQL query being executed. |
🔹 sql_execution_end
| Field | Type | Description |
|---|---|---|
state | "sql_execution_end" | SQL execution completed. No additional fields. |
Example
data: {
"type": "state",
"data": {
"state": "sql_generation_start",
"question": "list 5 customers",
"threadId": "0625991d-1bba-407d-8ad4-dd0210172484",
"language": "English"
},
"timestamp": 1751014954142
}
data: {
"type": "state",
"data": {
"state": "sql_generation_understanding",
"pollCount": 1,
"rephrasedQuestion": null,
"intentReasoning": null,
"sqlGenerationReasoning": null,
"retrievedTables": null,
"invalidSql": null,
"traceId": "f218b1f7-4623-4a56-8b66-18d544797b20"
},
"timestamp": 1751014954165
}
data: {
"type": "state",
"data": {
"state": "sql_generation_searching",
"pollCount": 4,
"rephrasedQuestion": "List 5 customers from the olist_customers_dataset table.",
"intentReasoning": "User wants to retrieve specific customer data, likely using SQL query.",
"sqlGenerationReasoning": null,
"retrievedTables": null,
"invalidSql": null,
"traceId": "f218b1f7-4623-4a56-8b66-18d544797b20"
},
"timestamp": 1751014957183
}
data: {
"type": "state",
"data": {
"state": "sql_generation_planning",
"pollCount": 6,
"rephrasedQuestion": "List 5 customers from the olist_customers_dataset table.",
"intentReasoning": "User wants to retrieve specific customer data, likely using SQL query.",
"sqlGenerationReasoning": null,
"retrievedTables": ["olist_customers_dataset"],
"invalidSql": null,
"traceId": "f218b1f7-4623-4a56-8b66-18d544797b20"
},
"timestamp": 1751014959232
}
data: {
"type": "state",
"data": {
"state": "sql_generation_generating",
"pollCount": 9,
"rephrasedQuestion": "List 5 customers from the olist_customers_dataset table.",
"intentReasoning": "User wants to retrieve specific customer data, likely using SQL query.",
"sqlGenerationReasoning": "1. **Identify the table involved**: The question asks for customer data, so the relevant table is `olist_customers_dataset`.\n\n2. **Determine the number of records needed**: The user requests 5 customers, so we need to select 5 entries from the table.",
"retrievedTables": ["olist_customers_dataset"],
"invalidSql": null,
"traceId": "f218b1f7-4623-4a56-8b66-18d544797b20"
},
"timestamp": 1751014962254
}
data: {
"type": "state",
"data": {
"state": "sql_generation_success",
"sql": "SELECT \"o\".\"customer_id\", \"o\".\"customer_zip_code_prefix\", \"o\".\"customer_city\", \"o\".\"customer_state\" FROM \"olist_customers_dataset\" AS \"o\" LIMIT 5"
},
"timestamp": 1751014963263
}
data: {
"type": "state",
"data": {
"state": "sql_execution_start",
"sql": "SELECT \"o\".\"customer_id\", \"o\".\"customer_zip_code_prefix\", \"o\".\"customer_city\", \"o\".\"customer_state\" FROM \"olist_customers_dataset\" AS \"o\" LIMIT 5"
},
"timestamp": 1751014963263
}
data: {
"type": "state",
"data": {
"state": "sql_execution_end"
},
"timestamp": 1751014963339
}
content_block_start
- Purpose: Signals the beginning of a streamed text block. The
nametells you which one:summary_generation— a natural-language summary of the SQL result.explanation— a natural-language answer to a non-SQL question (no SQL is generated in this case).
- Payload: Describes the block (always
type: "text").
{
"type": "content_block_start",
"content_block": {
"type": "text",
"name": "summary_generation"
}
}content_block_delta
- Purpose: Streams the actual content in parts (e.g., the summary or explanation, chunk by chunk).
- Payload: Includes a
text_deltawith the partial content.
{
"type": "content_block_delta",
"delta": {
"type": "text_delta",
"text": "São Paulo (SP) leads with"
}
}content_block_stop
- Purpose: Indicates the end of a streamed content block.
{
"type": "content_block_stop"
}pendingQuestion
- Purpose: The AI paused at a clarification checkpoint and needs your input before continuing. The stream stays open; answer via
POST /v2/stream/ask/respond. - Payload:
| Field | Type | Description |
|---|---|---|
questionId | string | Identifier of this clarification question. Echo it back in respond. |
checkpoint | "intent" | "sql_reasoning" | Where in the pipeline the AI paused. |
question | string | The clarification question to show the user. |
options | Array<{ value: string; label: string }> | Selectable options (may be empty for free-text answers). |
selectionType | "single" | "multi" | Whether one or multiple options may be selected. |
rationale | string | undefined | Why the AI is asking. |
queryId | string | Per-turn query id. Pass it to respond to route the answer to this turn. |
threadId | string | The thread this turn belongs to. |
{
"type": "pendingQuestion",
"data": {
"questionId": "b8f0c1e2-1a2b-4c3d-9e8f-7a6b5c4d3e2f",
"checkpoint": "intent",
"question": "Which \"customers\" did you mean?",
"options": [
{ "value": "all", "label": "All registered customers" },
{ "value": "active", "label": "Only active customers" }
],
"selectionType": "single",
"rationale": "The question is ambiguous about customer status.",
"queryId": "1f0a3c7d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
"threadId": "0625991d-1bba-407d-8ad4-dd0210172484"
},
"timestamp": 1751014955000
}message_stop
- Purpose: Marks the end of the entire stream. Includes processing
duration(ms) andthreadId.
{
"type": "message_stop",
"data": {
"threadId": "0625991d-1bba-407d-8ad4-dd0210172484",
"duration": 11096
}
}Error Handling
If an error occurs at any stage of the pipeline, the server emits a type: "error" frame (followed by a terminal message_stop). This lets clients gracefully detect and handle failures — e.g., show an error message in the UI, add retry logic, or cancel streaming.
Error Event Structure
{
"type": "error",
"data": {
"code": "INVALID_SQL_ERROR",
"error": "Unrecognized token near FROM"
},
"timestamp": 1751015028888
}| Field | Type | Description |
|---|---|---|
code | string | Machine-readable error code (e.g. INVALID_SQL_ERROR, INTERNAL_SERVER_ERROR). |
error | string | Human-readable error message. |
Additional context fields (such as
traceId) may accompanydatadepending on the error.
Pre-stream errors. Validation failures that happen before the SSE stream opens are returned as a normal JSON error response instead of an
errorframe —400(missingprojectId/question),401(invalid API key),404(project not found), or409(a turn is already in progress for this thread).
