Docs menuThe turns/v1 contract

The turns/v1 contract

The columns a custom agent query returns.

Updated 3 October 2026

Columns

Your query returns one result set with one row per turn. A turn is one question and the agent’s answer to it. All the columns in the table below are columns of that single row.

The agent’s steps, such as the SQL it ran, are not separate tables either. They are JSON arrays in columns of the same row.

A result with two turns looks like this. It shows five columns; a real result has more, up to the full list below.

turn_id | asked_at         | question_text       | answer_text   | statements      | …
--------+------------------+---------------------+---------------+-----------------+--
101     | 2026-10-02 12:00 | Orders last week?   | 1,204 orders. | [{"sql": "…"}]  | …
102     | 2026-10-02 12:05 | How many employees? | I can't tell. | []              | …

If your agent stores turns across several tables, as most do, join and aggregate them in the query. The database pages show how for each database.

Column names are not case-sensitive. Only the first four columns are required.

If your agent never records a value, leave the column out. If it records the value but a turn has none, return NULL, or [] for an array. A missing column is shown as not available for the agent; a NULL is shown as empty for that turn.

This matters most for statements. If your agent doesn’t expose its SQL, leave the column out. If it does, return [] for a turn that ran no SQL.

ColumnTypeRequiredDescription
turn_idtextYesA unique, stable ID for the turn. Don’t change it when you edit the query.
asked_attimestampYesWhen the question was asked. A timestamp without a time zone is read as UTC.
question_texttextYesThe user’s question.
answer_texttextYes, can be NULLThe answer the user saw. NULL if there was none, for example after an error.
updated_attimestampNoWhen the row last changed. See Incremental reads.
completed_attimestampNoWhen the answer finished. Used for latency.
outcometextNoHow the turn ended. success, ok and completed count as completed; error, failed and failure as failed; running and in_progress as in progress. Other values are kept and shown as unknown.
error_typetextNoThe error class, if the turn failed.
error_messagetextNoThe error message, if the turn failed.
conversation_idtextNoGroups turns into a conversation. Leave it out if your agent has no conversations.
positionintegerNoThe turn’s position in the conversation, starting at 1. Defaults to the order of asked_at.
user_idtextNoThe ID of the user who asked.
user_nametextNoTheir display name.
user_emailtextNoTheir email address.
user_roletextNoThe database or platform role the agent used for them.
client_interfacetextNoWhere the question was asked, such as slack, web or api.
session_idtextNoThe client session ID.
model_nametextNoThe model that answered. Needed to price tokens.
input_tokensintegerNoInput tokens for the turn.
output_tokensintegerNoOutput tokens for the turn.
semantic_model_reftextNoThe semantic model or layer the agent used.
statementsJSON arrayNoSQL the agent generated or ran. See statements.
reasoningJSON arrayNoThe agent’s reasoning steps. See reasoning.
messagesJSON arrayNoAnything else the agent said, such as follow-up suggestions. See messages.
tool_callsJSON arrayNoOther tools the agent used. See tool_calls.
extraJSONNoAnything else you want stored. We keep it but don’t use it yet.

JSON columns

Each of statements, reasoning, messages and tool_calls holds a JSON array of objects, one object per step, in the order the steps happened. Extra keys are ignored. Each section lists the keys and ends with a complete example value for one turn.

statements

KeyTypeDescription
sqltextRequired. The statement. An empty one is ignored, so a turn that wrote no SQL still reads as one.
statustextsucceeded, failed, not_executed, or your own value.
errortextThe error message, if it failed.
rowsintegerRows returned.
started_at, ended_attimestampWhen it started and finished, in ISO 8601.
is_answerbooleanMarks the statement the answer is based on. Defaults to the last one that succeeded.
resultJSONA small sample of what it returned, in any shape: {"columns": [...], "rows": [[...]]}, [{"count": 1204}] or your own. Kept as given.

Example, for a turn where the first query failed and the retry answered the question:

[
  {
    "sql": "SELECT count(*) FROM orders WHERE week = 39",
    "status": "failed",
    "error": "column week does not exist",
    "rows": null,
    "started_at": "2026-10-02T12:00:01Z",
    "ended_at": "2026-10-02T12:00:02Z",
    "is_answer": false
  },
  {
    "sql": "SELECT count(*) FROM orders WHERE order_week = 39",
    "status": "succeeded",
    "error": null,
    "rows": 1,
    "started_at": "2026-10-02T12:00:02Z",
    "ended_at": "2026-10-02T12:00:03Z",
    "is_answer": true,
    "result": {"columns": ["count"], "rows": [[1204]]}
  }
]

reasoning

KeyTypeDescription
texttextRequired.
labeltextrestatement, assumption, data_sourcing, plan, deliberation, or your own value, shown as other.

Example:

[
  {"label": "plan", "text": "Count the orders shipped in ISO week 39."},
  {"label": "deliberation", "text": "There is no week column. Use order_week instead."}
]

messages

KeyTypeDescription
texttextRequired.
kindtextclarifying_question, suggested_questions, visualization, restatement, or your own value, shown as other.

Example:

[
  {"kind": "suggested_questions", "text": "How many of those orders were late?"}
]

tool_calls

KeyTypeDescription
nametextRequired.
input, outputJSONWhat the tool received and returned.
statustextHow the call ended.
started_at, ended_attimestampWhen it started and finished, in ISO 8601.

Example:

[
  {
    "name": "search_docs",
    "input": {"query": "shipping week"},
    "output": {"tables": ["orders"]},
    "status": "ok",
    "started_at": "2026-10-02T12:00:00Z",
    "ended_at": "2026-10-02T12:00:01Z"
  }
]

Incremental reads

Each run reads only the rows that are new or changed since the last run, with a one-hour overlap.

We find them with updated_at, or with asked_at if you don’t return updated_at. If a row can change after it’s first written, for example when the answer arrives later, return updated_at and update it on every change. Otherwise, only include a turn once it’s finished.

Rows you delete from your database are not deleted from Benchouse.

Versioning

This is version turns/v1. Adding optional columns doesn’t change the version. Each stored row records the version and the columns it was read with, so changing your query doesn’t affect turns already stored.