> ## Documentation Index
> Fetch the complete documentation index at: https://docs.braintrust.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Query by SQL

> Query logs, experiments, and datasets with SQL through the REST API.

The `/btql` endpoint runs [SQL](https://docs.braintrust.dev/reference/sql.md) over your logs, experiments, and datasets and returns the rows in the response. If a query times out, try narrowing it first (see [SQL best practices](https://docs.braintrust.dev/reference/sql/best-practices.md)). For large exports, or a well-scoped query that still times out, use an [async query](#async-queries).

> [!TIP]
>
> You can also run SQL queries interactively on the [\*\* Query\*\*](https://www.braintrust.dev/app/~/sql) page, or with the [`bt sql`](https://docs.braintrust.dev/reference/cli/sql) CLI command.

## Base URL

The base URL depends on your organization's [data plane region](https://docs.braintrust.dev/admin/organizations.md#data-plane-region):

| Region | Base URL |
| - | - |
| US | `https://api.braintrust.dev` |
| EU | `https://api-eu.braintrust.dev` |
| Self-hosted | Your custom data plane URL |

Authenticate every request with your API key in the `Authorization` header. The examples below use the US base URL.

## Query data

Send a `POST /btql` request with a [SQL](https://docs.braintrust.dev/reference/sql.md) `query` in the body.

Braintrust runs the query synchronously and returns the results in a single response.

**Example**

```bash curl icon="terminal" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
curl -L https://api.braintrust.dev/btql \
  -H "Authorization: Bearer $BRAINTRUST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "SELECT id, input, output, scores FROM project_logs('\''your-project-id'\'', shape => '\''traces'\'') WHERE scores.accuracy > 0.8 LIMIT 100",
    "fmt": "json"
  }'
```

```typescript TypeScript icon="https://mintcdn.com/braintrust/c7ni0sUjwFCk4A6a/images/sdk-icons/typescript.svg?fit=max&auto=format&n=c7ni0sUjwFCk4A6a&q=85&s=7435e9525dad7c7d78d0dc33dc4bd18d" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
const API_URL = "https://api.braintrust.dev";
const headers = {
  Authorization: `Bearer ${process.env.BRAINTRUST_API_KEY}`,
  "Content-Type": "application/json",
};

const query = `
  SELECT id, input, output, scores
  FROM project_logs('your-project-id', shape => 'traces')
  WHERE scores.accuracy > 0.8
  LIMIT 100
`;

const response = await fetch(`${API_URL}/btql`, {
  method: "POST",
  headers,
  body: JSON.stringify({ query, fmt: "json" }),
});

const { data } = await response.json();
for (const row of data) {
  console.log(row);
}
```

```python Python icon="https://mintcdn.com/braintrust/c7ni0sUjwFCk4A6a/images/sdk-icons/python.svg?fit=max&auto=format&n=c7ni0sUjwFCk4A6a&q=85&s=1d27736793336d98f35e75c83f61a4ed" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import os

import requests

API_URL = "https://api.braintrust.dev"
headers = {"Authorization": "Bearer " + os.environ["BRAINTRUST_API_KEY"]}

query = """
SELECT id, input, output, scores
FROM project_logs('your-project-id', shape => 'traces')
WHERE scores.accuracy > 0.8
LIMIT 100
"""

response = requests.post(
    f"{API_URL}/btql",
    headers=headers,
    json={"query": query, "fmt": "json"},
).json()

for row in response["data"]:
    print(row)
```

**Request**

Send a JSON request body with these fields:

| Field | Type | Required | Description |
| - | - | - | - |
| `query` | string or object | Yes | The query to run. Pass a [SQL](https://docs.braintrust.dev/reference/sql.md) string, or a structured query object. Data-source functions like `project_logs()` and `experiment()` accept an object name or its ID. See [Querying by name](https://docs.braintrust.dev/reference/sql/query-structure.md#querying-by-name). To fetch more than one page, see [Pagination](#pagination). |
| `fmt` | `"json"` \| `"jsonl"` \| `"parquet"` | No | Output format. Defaults to `json`. |
| `lint_mode` | `"default"` \| `"strict"` | No | How [lint warnings](https://docs.braintrust.dev/reference/sql/best-practices.md#lint-warnings) surface. `default` returns them alongside results; `strict` blocks the query when any warning fires. |
| `version` | string | No | Query the data as of a specific version (an `_xact_id`). Supported for `experiment` and `dataset` sources; not for `project_logs`. Useful for [recovering deleted rows](https://docs.braintrust.dev/kb/recovering-deleted-experiment-rows). |
| `tz_offset` | number | No | Timezone offset in minutes for time-based operations, following the `Date.prototype.getTimezoneOffset()` convention (positive is behind UTC). |

> [!NOTE]
>
> For correct day boundaries, set `tz_offset` to match your timezone. For example, use `480` for US Pacific Standard Time.

**Response**

The response format depends on `fmt`:

* `json` (default): an object with a `data` array of row objects. Each row contains the columns your query selects, or every column for `SELECT *`.
* `jsonl`: one row per line, for streaming.
* `parquet`: a binary column format, suited to large exports.

```json JSON wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
{
  "data": [
    {
      "id": "6f3b1c9e-...",
      "input": { "question": "What is 2+2?" },
      "output": "4",
      "scores": { "accuracy": 1.0 }
    }
  ],
  "schema": { ... }
}
```

| Field | Type | Description |
| - | - | - |
| `data` | array of objects | The result rows. Present for the `json` format. |
| `schema` | object | A JSON Schema describing the result columns, generated from the columns your query selects. Present for `json` responses; omitted for `jsonl` and `parquet`. |

To fetch more rows than fit in one response, see [Pagination](#pagination).

### Pagination

Each response returns one page of rows. To fetch every page:

1. Run the query, and read the cursor from the `x-bt-cursor` response header.
2. Resend the same query with the cursor:
   * **SQL string**: append `OFFSET '<cursor>'`. See [Syntax styles](https://docs.braintrust.dev/reference/sql.md#syntax-styles).
   * **Structured query object**: set its `cursor` field.
3. Repeat until a response has no cursor.

> [!NOTE]
>
> On Braintrust-hosted deployments, large responses redirect to a stored copy of the results. Follow the redirect (for example, with `curl -L`), and read the cursor from the `x-amz-meta-bt_cursor` header instead.

**Example**

This example fetches every row of a dataset with `SELECT * FROM dataset(...)`.

```bash curl icon="terminal" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
# First page. -i prints the response headers, including the cursor
# (x-bt-cursor, or x-amz-meta-bt_cursor after a redirect).
curl -i -L https://api.braintrust.dev/btql \
  -H "Authorization: Bearer $BRAINTRUST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "SELECT * FROM dataset(['\''your-project-name'\'', '\''your-dataset-name'\'']) LIMIT 100",
    "fmt": "json"
  }'

# Next page. Add the cursor from the previous response as OFFSET.
curl -i -L https://api.braintrust.dev/btql \
  -H "Authorization: Bearer $BRAINTRUST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "SELECT * FROM dataset(['\''your-project-name'\'', '\''your-dataset-name'\'']) LIMIT 100 OFFSET '\''<cursor>'\''",
    "fmt": "json"
  }'
```

```typescript TypeScript icon="https://mintcdn.com/braintrust/c7ni0sUjwFCk4A6a/images/sdk-icons/typescript.svg?fit=max&auto=format&n=c7ni0sUjwFCk4A6a&q=85&s=7435e9525dad7c7d78d0dc33dc4bd18d" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
const API_URL = "https://api.braintrust.dev";
const headers = {
  Authorization: `Bearer ${process.env.BRAINTRUST_API_KEY}`,
  "Content-Type": "application/json",
};

// Quote a value as a SQL string literal, escaping single quotes.
const sqlString = (value: string) => `'${value.replaceAll("'", "''")}'`;

async function* fetchAllRows(projectName: string, datasetName: string) {
  let cursor: string | null = null;
  do {
    const names = `${sqlString(projectName)}, ${sqlString(datasetName)}`;
    let query = `SELECT * FROM dataset([${names}]) LIMIT 100`;
    if (cursor) {
      query += ` OFFSET '${cursor}'`;
    }
    const res = await fetch(`${API_URL}/btql`, {
      method: "POST",
      headers,
      body: JSON.stringify({ query, fmt: "json" }),
    });
    if (!res.ok) {
      throw new Error(`Query failed: ${res.status}: ${await res.text()}`);
    }
    const { data } = await res.json();
    yield* data;
    // Large responses redirect to stored results, which carry the cursor
    // in x-amz-meta-bt_cursor.
    cursor =
      res.headers.get("x-bt-cursor") ??
      res.headers.get("x-amz-meta-bt_cursor");
  } while (cursor);
}

const rows = fetchAllRows("your-project-name", "your-dataset-name");
for await (const row of rows) {
  console.log(row);
}
```

```python Python icon="https://mintcdn.com/braintrust/c7ni0sUjwFCk4A6a/images/sdk-icons/python.svg?fit=max&auto=format&n=c7ni0sUjwFCk4A6a&q=85&s=1d27736793336d98f35e75c83f61a4ed" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import os

import requests

API_URL = "https://api.braintrust.dev"
headers = {"Authorization": "Bearer " + os.environ["BRAINTRUST_API_KEY"]}


def sql_string(value: str) -> str:
    # Quote a value as a SQL string literal, escaping single quotes.
    return "'" + value.replace("'", "''") + "'"


def fetch_all_rows(project_name: str, dataset_name: str):
    cursor = None
    while True:
        names = f"{sql_string(project_name)}, {sql_string(dataset_name)}"
        query = f"SELECT * FROM dataset([{names}]) LIMIT 100"
        if cursor:
            query += f" OFFSET '{cursor}'"
        res = requests.post(
            f"{API_URL}/btql",
            headers=headers,
            json={"query": query, "fmt": "json"},
        )
        res.raise_for_status()
        yield from res.json()["data"]
        # Large responses redirect to stored results, which carry the cursor
        # in x-amz-meta-bt_cursor.
        cursor = res.headers.get("x-bt-cursor") or res.headers.get(
            "x-amz-meta-bt_cursor"
        )
        if not cursor:
            break


for row in fetch_all_rows("your-project-name", "your-dataset-name"):
    print(row)
```

> [!NOTE]
>
> If you use the Python or TypeScript SDK to read a dataset or experiment, you don't need this code. Iterate over the dataset or experiment, and the SDK handles the cursor for you.

### Timeouts

On Braintrust-hosted deployments, synchronous queries time out after 30 seconds. That's enough for interactive queries, dashboards, and most routine reads. If a query times out, try [optimizing it](https://docs.braintrust.dev/reference/sql/best-practices.md) first. For large exports, or a well-scoped query that still times out, use an [async query](#async-queries).

### Examples

> [accordion] Export data
>
> This example exports up to 100 traces from the last 7 days of a project's logs to `export.json` and `export.parquet`. You can export logs, experiments, or datasets to JSON or Parquet by setting `fmt`.
>
> Each request returns one page of results. Without a `LIMIT`, a `project_logs()` query returns the first 10 rows, or 10 traces with `shape => 'traces'`, and other sources return the first 1,000. To export more, follow the `x-bt-cursor` header as described in [Pagination](#pagination).
>
> ```bash curl icon="terminal" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
> # Export to JSON
> curl -fL https://api.braintrust.dev/btql \
>   -H "Authorization: Bearer $BRAINTRUST_API_KEY" \
>   -H "Content-Type: application/json" \
>   -d '{
>     "query": "SELECT * FROM project_logs('\''your-project-id'\'', shape => '\''traces'\'') WHERE created >= now() - interval 7 day LIMIT 100",
>     "fmt": "json"
>   }' > export.json
>
> # Export to Parquet
> curl -fL https://api.braintrust.dev/btql \
>   -H "Authorization: Bearer $BRAINTRUST_API_KEY" \
>   -H "Content-Type: application/json" \
>   -d '{
>     "query": "SELECT * FROM project_logs('\''your-project-id'\'', shape => '\''traces'\'') WHERE created >= now() - interval 7 day LIMIT 100",
>     "fmt": "parquet"
>   }' > export.parquet
> ```
>
> ```typescript TypeScript icon="https://mintcdn.com/braintrust/c7ni0sUjwFCk4A6a/images/sdk-icons/typescript.svg?fit=max&auto=format&n=c7ni0sUjwFCk4A6a&q=85&s=7435e9525dad7c7d78d0dc33dc4bd18d" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
> import { writeFile } from "node:fs/promises";
>
> const API_URL = "https://api.braintrust.dev";
> const headers = {
>   Authorization: `Bearer ${process.env.BRAINTRUST_API_KEY}`,
>   "Content-Type": "application/json",
> };
>
> const query = `
>   SELECT *
>   FROM project_logs('your-project-id', shape => 'traces')
>   WHERE created >= now() - interval 7 day
>   LIMIT 100
> `;
>
> async function exportTo(fmt: "json" | "parquet", path: string) {
>   const res = await fetch(`${API_URL}/btql`, {
>     method: "POST",
>     headers,
>     body: JSON.stringify({ query, fmt }),
>   });
>   if (!res.ok) {
>     throw new Error(`Export failed: ${res.status}: ${await res.text()}`);
>   }
>   await writeFile(path, Buffer.from(await res.arrayBuffer()));
> }
>
> await exportTo("json", "export.json");
> await exportTo("parquet", "export.parquet");
> ```
>
> ```python Python icon="https://mintcdn.com/braintrust/c7ni0sUjwFCk4A6a/images/sdk-icons/python.svg?fit=max&auto=format&n=c7ni0sUjwFCk4A6a&q=85&s=1d27736793336d98f35e75c83f61a4ed" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
> import os
>
> import requests
>
> API_URL = "https://api.braintrust.dev"
> headers = {"Authorization": "Bearer " + os.environ["BRAINTRUST_API_KEY"]}
>
> query = """
> SELECT *
> FROM project_logs('your-project-id', shape => 'traces')
> WHERE created >= now() - interval 7 day
> LIMIT 100
> """
>
>
> def export_to(fmt: str, path: str) -> None:
>     response = requests.post(
>         f"{API_URL}/btql",
>         headers=headers,
>         json={"query": query, "fmt": fmt},
>     )
>     response.raise_for_status()
>     with open(path, "wb") as f:
>         f.write(response.content)
>
>
> export_to("json", "export.json")
> export_to("parquet", "export.parquet")
> ```
>
> > [!TIP]
> >
> > A large export takes many synchronous requests, and each one times out after 30 seconds. To export many thousands of rows, submit the query to the [async query endpoints](#async-queries) instead.

> [accordion] Fetch experiment results
>
> This example checks how much of an experiment has been reviewed. It returns a single row with two counts: `reviewed`, the number of rows with a value in your review score column, and `total`, the number of root spans, one per test case. Replace `response quality` with the name of your review score. You can adapt the query to check other experiment metrics.
>
> ```bash curl icon="terminal" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
> curl -L https://api.braintrust.dev/btql \
>   -H "Authorization: Bearer $BRAINTRUST_API_KEY" \
>   -H "Content-Type: application/json" \
>   -d '{
>     "query": "SELECT sum(CASE WHEN scores.\"response quality\" IS NOT NULL THEN 1 ELSE 0 END) AS reviewed, sum(CASE WHEN is_root THEN 1 ELSE 0 END) AS total FROM experiment('\''your-experiment-id'\'')",
>     "fmt": "json"
>   }'
> ```
>
> ```typescript TypeScript icon="https://mintcdn.com/braintrust/c7ni0sUjwFCk4A6a/images/sdk-icons/typescript.svg?fit=max&auto=format&n=c7ni0sUjwFCk4A6a&q=85&s=7435e9525dad7c7d78d0dc33dc4bd18d" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
> const API_URL = "https://api.braintrust.dev";
> const headers = {
>   Authorization: `Bearer ${process.env.BRAINTRUST_API_KEY}`,
>   "Content-Type": "application/json",
> };
>
> async function fetchExperimentReviewStatus(experimentId: string) {
>   // Replace "response quality" with the name of your review score column
>   const query = `
>     SELECT
>       sum(CASE WHEN scores."response quality" IS NOT NULL THEN 1 ELSE 0 END)
>         AS reviewed,
>       sum(CASE WHEN is_root THEN 1 ELSE 0 END) AS total
>     FROM experiment('${experimentId}')
>   `;
>
>   const res = await fetch(`${API_URL}/btql`, {
>     method: "POST",
>     headers,
>     body: JSON.stringify({ query, fmt: "json" }),
>   });
>   return res.json();
> }
>
> const EXPERIMENT_ID = "your-experiment-id";
> console.log(await fetchExperimentReviewStatus(EXPERIMENT_ID));
> ```
>
> ```python Python icon="https://mintcdn.com/braintrust/c7ni0sUjwFCk4A6a/images/sdk-icons/python.svg?fit=max&auto=format&n=c7ni0sUjwFCk4A6a&q=85&s=1d27736793336d98f35e75c83f61a4ed" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
> import os
>
> import requests
>
> API_URL = "https://api.braintrust.dev"
> headers = {"Authorization": "Bearer " + os.environ["BRAINTRUST_API_KEY"]}
>
>
> def fetch_experiment_review_status(experiment_id: str) -> dict:
>     # Replace "response quality" with the name of your review score column
>     query = f"""
>     SELECT
>       sum(CASE WHEN scores."response quality" IS NOT NULL THEN 1 ELSE 0 END)
>         AS reviewed,
>       sum(CASE WHEN is_root THEN 1 ELSE 0 END) AS total
>     FROM experiment('{experiment_id}')
>     """
>
>     return requests.post(
>         f"{API_URL}/btql",
>         headers=headers,
>         json={"query": query, "fmt": "json"},
>     ).json()
>
>
> EXPERIMENT_ID = "your-experiment-id"
> print(fetch_experiment_review_status(EXPERIMENT_ID))
> ```

> [accordion] Fetch child spans by trace metadata
>
> This example measures how long a particular step takes across traces that share a metadata value. It retrieves up to 10 traces whose metadata matches, including all of their child spans, then keeps the spans with a given name and prints each span's duration and the average, in seconds. Replace `your-org-name` with the metadata value to match, and `your-span-name` with the name of the span to measure.
>
> ```typescript TypeScript icon="https://mintcdn.com/braintrust/c7ni0sUjwFCk4A6a/images/sdk-icons/typescript.svg?fit=max&auto=format&n=c7ni0sUjwFCk4A6a&q=85&s=7435e9525dad7c7d78d0dc33dc4bd18d" expandable wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
> const API_URL = "https://api.braintrust.dev";
> const headers = {
>   Authorization: `Bearer ${process.env.BRAINTRUST_API_KEY}`,
>   "Content-Type": "application/json",
> };
>
> const PROJECT_ID = "your-project-id";
> const SPAN_NAME = "your-span-name";
>
> // Find all rows matching a certain metadata value
> const query = `
>   SELECT span_attributes, metrics
>   FROM project_logs('${PROJECT_ID}', shape => 'traces')
>   WHERE metadata.orgName = 'your-org-name'
>   LIMIT 10
> `;
>
> const res = await fetch(`${API_URL}/btql`, {
>   method: "POST",
>   headers,
>   body: JSON.stringify({ query, fmt: "json" }),
> });
> const { data } = await res.json();
>
> const durations: number[] = [];
> for (const span of data) {
>   if (span.span_attributes.name === SPAN_NAME) {
>     const { start, end } = span.metrics;
>     if (start && end) {
>       const duration = end - start;
>       durations.push(duration);
>       console.log(`Duration: ${duration.toFixed(3)}s`);
>     } else {
>       console.log("Start or end not found for this span");
>     }
>   }
> }
>
> if (durations.length > 0) {
>   const average = durations.reduce((a, b) => a + b, 0) / durations.length;
>   console.log(`\nAverage duration: ${average.toFixed(3)}s`);
> }
> ```
>
> ```python Python icon="https://mintcdn.com/braintrust/c7ni0sUjwFCk4A6a/images/sdk-icons/python.svg?fit=max&auto=format&n=c7ni0sUjwFCk4A6a&q=85&s=1d27736793336d98f35e75c83f61a4ed" expandable wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
> import os
>
> import requests
>
> API_URL = "https://api.braintrust.dev"
> headers = {"Authorization": "Bearer " + os.environ["BRAINTRUST_API_KEY"]}
>
> PROJECT_ID = "your-project-id"
> SPAN_NAME = "your-span-name"
>
> # Find all rows matching a certain metadata value
> query = f"""
> SELECT span_attributes, metrics
> FROM project_logs('{PROJECT_ID}', shape => 'traces')
> WHERE metadata.orgName = 'your-org-name'
> LIMIT 10
> """
>
> response = requests.post(
>     f"{API_URL}/btql", headers=headers, json={"query": query, "fmt": "json"}
> ).json()
>
> durations = []
> for span in response["data"]:
>     if span["span_attributes"]["name"] == SPAN_NAME:
>         metrics = span["metrics"]
>         if metrics.get("end") and metrics.get("start"):
>             duration = metrics["end"] - metrics["start"]
>             durations.append(duration)
>             print(f"Duration: {duration:.3f}s")
>         else:
>             print("Start or end not found for this span")
>
> if durations:
>     print(f"\nAverage duration: {sum(durations) / len(durations):.3f}s")
> ```

## Async queries

> [!NOTE]
>
> Async queries are only available on the [Enterprise plan](https://docs.braintrust.dev/plans-and-limits.md#plans).

Async queries run in the background, so they aren't limited by the synchronous timeout. Braintrust stores the results, and you fetch them in pages. Use an async query for:

* A large export or backfill that returns many thousands of rows.
* A well-scoped query that still times out as a synchronous request.

Async queries process rows in batches, in the order Braintrust stores them, so they can select and filter rows but can't aggregate, deduplicate, or sort them. `count()`, `sum()`, `GROUP BY`, `SELECT DISTINCT`, `ORDER BY`, and `OFFSET` aren't supported.

Async queries address request timeouts and long runtimes. They aren't a substitute for query optimization, so narrow a broad query before running it asynchronously. See [SQL best practices](https://docs.braintrust.dev/reference/sql/best-practices.md).

### Limits

**Braintrust-hosted**

* **Runtime**: 1 hour of cumulative runtime.
* **Result size**: 10 GiB.
* **Retention**: 7 days. Fetch the results within that window.
* **Concurrency**: Each organization can have a limited number of async queries queued or running at the same time. When your organization reaches the limit, `POST /btql/async` returns a `429 Too Many Requests` error. Submit the query again after one of your async queries finishes or is canceled.

**Self-hosted**

* **Requirements**: data plane v2.3.0 or later (Helm chart 6.4.0 or later). If you deploy with the AWS Terraform module, use v6.0.0 or later and set `enable_ecs_api = true`. With the default setting, the async endpoints aren't reachable.
* **Runtime**: 1 hour of cumulative runtime by default.
* **Result size**: 200 GiB by default on data plane v2.17.0 and later (Helm chart 6.20.0 or Terraform module v6.9.0 and later), and 10 GiB on earlier versions.

A query that exceeds the runtime or result size limit stops and reports a `failed` status with an explanatory `error`.

### Submit a query

`POST /btql/async` submits a query and returns its `id`, with a `status` of `queued`.

**Example**

```bash curl icon="terminal" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
curl https://api.braintrust.dev/btql/async \
  -H "Authorization: Bearer $BRAINTRUST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "SELECT id, input, output, scores FROM project_logs('\''your-project-id'\'', shape => '\''traces'\'') WHERE scores.accuracy > 0.8"
  }'
```

```typescript TypeScript icon="https://mintcdn.com/braintrust/c7ni0sUjwFCk4A6a/images/sdk-icons/typescript.svg?fit=max&auto=format&n=c7ni0sUjwFCk4A6a&q=85&s=7435e9525dad7c7d78d0dc33dc4bd18d" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
const API_URL = "https://api.braintrust.dev";
const headers = {
  Authorization: `Bearer ${process.env.BRAINTRUST_API_KEY}`,
  "Content-Type": "application/json",
};

const query = `
  SELECT id, input, output, scores
  FROM project_logs('your-project-id', shape => 'traces')
  WHERE scores.accuracy > 0.8
`;

const res = await fetch(`${API_URL}/btql/async`, {
  method: "POST",
  headers,
  body: JSON.stringify({ query }),
});
if (!res.ok) {
  throw new Error(`Submit failed: ${res.status}: ${await res.text()}`);
}
const { id } = await res.json();
console.log(id);
```

```python Python icon="https://mintcdn.com/braintrust/c7ni0sUjwFCk4A6a/images/sdk-icons/python.svg?fit=max&auto=format&n=c7ni0sUjwFCk4A6a&q=85&s=1d27736793336d98f35e75c83f61a4ed" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import os

import requests

API_URL = "https://api.braintrust.dev"
headers = {"Authorization": "Bearer " + os.environ["BRAINTRUST_API_KEY"]}

query = """
SELECT id, input, output, scores
FROM project_logs('your-project-id', shape => 'traces')
WHERE scores.accuracy > 0.8
"""

submit = requests.post(
    f"{API_URL}/btql/async", headers=headers, json={"query": query}
)
submit.raise_for_status()
query_id = submit.json()["id"]
print(query_id)
```

> [!TIP]
>
> You can also submit async queries with the [`bt sql --async`](https://docs.braintrust.dev/reference/cli/sql#async-queries) CLI command. By default, it waits for the query to finish and prints the results. Pass `--no-wait` to print the submit response, including the query's `id`, and exit instead.

**Request**

Send a JSON request body with these fields:

| Field | Type | Required | Description |
| - | - | - | - |
| `query` | string | Yes | The [SQL](https://docs.braintrust.dev/reference/sql.md) query to run. The `FROM` clause must reference exactly one object by its ID, such as `project_logs('<project-id>')`. Object names, which work in synchronous queries, aren't supported. |
| `batch_size` | number | No | Rows to write per execution batch, up to 100,000. |

Unlike `POST /btql`, the async endpoint ignores `fmt`, `lint_mode`, `version`, and `tz_offset`, and `query` must be a string rather than a structured query object.

**Response**

```json JSON wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
{
  "id": "4f9c2e1a-...",
  "project_id": "8b7d3c2e-...",
  "object_id": "project_logs:8b7d3c2e-...",
  "status": "queued"
}
```

The response includes:

| Field | Type | Description |
| - | - | - |
| `id` | string | The query's ID. Pass it to the other async endpoints. |
| `project_id` | string | The ID of the project that contains the queried object. |
| `object_id` | string | The queried object, such as `project_logs:<project-id>`. |
| `status` | string | Always `queued` for a newly submitted query. |

### Check status

`GET /btql/async/:id` returns the query's progress. Poll it until the status is `succeeded`, `failed`, or `canceled`.

**Example**

```bash curl icon="terminal" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
curl https://api.braintrust.dev/btql/async/<query-id> \
  -H "Authorization: Bearer $BRAINTRUST_API_KEY"
```

```typescript TypeScript icon="https://mintcdn.com/braintrust/c7ni0sUjwFCk4A6a/images/sdk-icons/typescript.svg?fit=max&auto=format&n=c7ni0sUjwFCk4A6a&q=85&s=7435e9525dad7c7d78d0dc33dc4bd18d" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
const API_URL = "https://api.braintrust.dev";
const headers = {
  Authorization: `Bearer ${process.env.BRAINTRUST_API_KEY}`,
  "Content-Type": "application/json",
};

const queryId = "your-query-id";

let status = "queued";
let error: string | null = null;
while (status === "queued" || status === "running") {
  await new Promise((resolve) => setTimeout(resolve, 2000));
  const res = await fetch(`${API_URL}/btql/async/${queryId}`, { headers });
  if (!res.ok) {
    throw new Error(`Status check failed: ${res.status}`);
  }
  ({ status, error } = await res.json());
}
if (status !== "succeeded") {
  throw new Error(`Async query ${status}: ${error}`);
}
```

```python Python icon="https://mintcdn.com/braintrust/c7ni0sUjwFCk4A6a/images/sdk-icons/python.svg?fit=max&auto=format&n=c7ni0sUjwFCk4A6a&q=85&s=1d27736793336d98f35e75c83f61a4ed" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import os
import time

import requests

API_URL = "https://api.braintrust.dev"
headers = {"Authorization": "Bearer " + os.environ["BRAINTRUST_API_KEY"]}

query_id = "your-query-id"

status = "queued"
while status in ("queued", "running"):
    time.sleep(2)
    response = requests.get(
        f"{API_URL}/btql/async/{query_id}", headers=headers
    )
    response.raise_for_status()
    result = response.json()
    status = result["status"]
if status != "succeeded":
    raise RuntimeError(f"Async query {status}: {result.get('error')}")
```

**Request**

No request body. Pass the query's `id` in the path.

**Response**

```json JSON wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
{
  "automation_id": "4f9c2e1a-...",
  "project_id": "8b7d3c2e-...",
  "object_id": "project_logs:8b7d3c2e-...",
  "status": "succeeded",
  "stats": {
    "rows": 70,
    "bytes": 6599,
    "duration_ms": 3,
    "batches": 1
  },
  "error": null
}
```

The response includes:

| Field | Type | Description |
| - | - | - |
| `automation_id` | string | The query's ID, the same value as `id` from the submit response. |
| `project_id` | string | The ID of the project that contains the queried object. |
| `object_id` | string | The queried object, such as `project_logs:<project-id>`. |
| `status` | string | `queued`, `running`, `succeeded`, `failed`, or `canceled`. |
| `error` | string or null | The most recent error. Set when the query fails or is canceled, and can also be set while Braintrust retries a failed batch. |
| `stats` | object or null | Progress so far, including `rows`, `bytes`, and `duration_ms`. Null until the query starts writing results. |

### Fetch results

`GET /btql/async/:id/result` returns a page of results as `rows`, along with a `cursor` for the next page. Results are complete only after `status` is `succeeded`. Before then, this endpoint returns whatever rows are available so far.

**Example**

```bash curl icon="terminal" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
# First page
curl "https://api.braintrust.dev/btql/async/<query-id>/result?limit=1000" \
  -H "Authorization: Bearer $BRAINTRUST_API_KEY"

# Next page, using the cursor from the previous response
curl "https://api.braintrust.dev/btql/async/<query-id>/result?limit=1000&cursor=<cursor>" \
  -H "Authorization: Bearer $BRAINTRUST_API_KEY"
```

```typescript TypeScript icon="https://mintcdn.com/braintrust/c7ni0sUjwFCk4A6a/images/sdk-icons/typescript.svg?fit=max&auto=format&n=c7ni0sUjwFCk4A6a&q=85&s=7435e9525dad7c7d78d0dc33dc4bd18d" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
const API_URL = "https://api.braintrust.dev";
const headers = {
  Authorization: `Bearer ${process.env.BRAINTRUST_API_KEY}`,
  "Content-Type": "application/json",
};

const queryId = "your-query-id";

let cursor: string | null = null;
do {
  const url = new URL(`${API_URL}/btql/async/${queryId}/result`);
  url.searchParams.set("limit", "1000");
  if (cursor) {
    url.searchParams.set("cursor", cursor);
  }
  const res = await fetch(url, { headers });
  if (!res.ok) {
    throw new Error(`Fetching results failed: ${res.status}`);
  }
  const page = await res.json();
  for (const row of page.rows) {
    console.log(row);
  }
  cursor = page.cursor;
} while (cursor);
```

```python Python icon="https://mintcdn.com/braintrust/c7ni0sUjwFCk4A6a/images/sdk-icons/python.svg?fit=max&auto=format&n=c7ni0sUjwFCk4A6a&q=85&s=1d27736793336d98f35e75c83f61a4ed" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import os

import requests

API_URL = "https://api.braintrust.dev"
headers = {"Authorization": "Bearer " + os.environ["BRAINTRUST_API_KEY"]}

query_id = "your-query-id"

cursor = None
while True:
    params = {"limit": 1000}
    if cursor:
        params["cursor"] = cursor
    response = requests.get(
        f"{API_URL}/btql/async/{query_id}/result",
        headers=headers,
        params=params,
    )
    response.raise_for_status()
    page = response.json()
    for row in page["rows"]:
        print(row)
    cursor = page.get("cursor")
    if not cursor:
        break
```

**Request**

Pass the query's `id` in the path, and these URL query parameters:

| Field | Type | Required | Description |
| - | - | - | - |
| `limit` | number | No | Rows per page. Defaults to 1,000 and is capped at 10,000. |
| `cursor` | string | No | The `cursor` from the previous page. Repeat until `cursor` is null. |

**Response**

```json JSON wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
{
  "status": "succeeded",
  "rows": [
    {
      "id": "6f3b1c9e-...",
      "input": { "question": "What is 2+2?" },
      "output": "4",
      "scores": { "accuracy": 1.0 }
    }
  ],
  "cursor": "0:1",
  "error": null
}
```

The response includes:

| Field | Type | Description |
| - | - | - |
| `rows` | array of objects | The rows on this page. Each row contains the columns your query selects, or every column for `SELECT *`. |
| `cursor` | string or null | Pass this as `cursor` to fetch the next page. Null when you've reached the end of the rows available so far. The results are complete only after `status` is `succeeded`. |
| `status` | string | The query's status, as returned by the [status endpoint](#check-status). |
| `error` | string or null | The most recent error. Set when the query fails or is canceled, and can also be set while Braintrust retries a failed batch. |

A page can contain fewer than `limit` rows, so don't treat a short page as the last one.

### Cancel a query

`POST /btql/async/:id/cancel` cancels a queued or running query. Canceling a query that already finished discards its results. Only the user who submitted the query, or a user with `update` permission on the organization, can cancel it.

**Example**

```bash curl icon="terminal" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
curl -X POST https://api.braintrust.dev/btql/async/<query-id>/cancel \
  -H "Authorization: Bearer $BRAINTRUST_API_KEY"
```

```typescript TypeScript icon="https://mintcdn.com/braintrust/c7ni0sUjwFCk4A6a/images/sdk-icons/typescript.svg?fit=max&auto=format&n=c7ni0sUjwFCk4A6a&q=85&s=7435e9525dad7c7d78d0dc33dc4bd18d" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
const API_URL = "https://api.braintrust.dev";
const headers = {
  Authorization: `Bearer ${process.env.BRAINTRUST_API_KEY}`,
  "Content-Type": "application/json",
};

const queryId = "your-query-id";

const res = await fetch(`${API_URL}/btql/async/${queryId}/cancel`, {
  method: "POST",
  headers,
});
if (!res.ok) {
  throw new Error(`Cancel failed: ${res.status}`);
}
console.log(await res.json());
```

```python Python icon="https://mintcdn.com/braintrust/c7ni0sUjwFCk4A6a/images/sdk-icons/python.svg?fit=max&auto=format&n=c7ni0sUjwFCk4A6a&q=85&s=1d27736793336d98f35e75c83f61a4ed" wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import os

import requests

API_URL = "https://api.braintrust.dev"
headers = {"Authorization": "Bearer " + os.environ["BRAINTRUST_API_KEY"]}

query_id = "your-query-id"

response = requests.post(
    f"{API_URL}/btql/async/{query_id}/cancel", headers=headers
)
response.raise_for_status()
print(response.json())
```

**Request**

No request body. Pass the query's `id` in the path.

**Response**

```json JSON wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
{
  "id": "4f9c2e1a-...",
  "status": "canceled"
}
```

The response includes:

| Field | Type | Description |
| - | - | - |
| `id` | string | The query's ID. |
| `status` | string | Always `canceled`. |

## Next steps

* Learn the [SQL syntax](https://docs.braintrust.dev/reference/sql.md) for querying logs, experiments, and datasets.
* Follow [SQL best practices](https://docs.braintrust.dev/reference/sql/best-practices.md) to keep queries fast and within limits.
* Run queries with the [`bt sql`](https://docs.braintrust.dev/reference/cli/sql) CLI command.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.