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

# SQL over HTTP

> Run read-only SQL against your Provider Pools with plain HTTP and JSON: find a table, describe it, plan a query, and run it.

The Workspace API runs read-only SQL with JSON in and JSON out. You do not
need a Flight SQL client. These operations suit scripts, serverless functions,
and AI agents that read Provider data. Each result fits in a fixed size, so a
script or a model can read every response whole.

| Operation | What it does |
| - | - |
| `POST /api/provider-pool/search` | Finds the tables and columns that hold some kind of data, or lists the tables of a Provider. |
| `POST /api/provider-pool/describe-table` | Returns one table's columns, types, and key. |
| `POST /api/provider-pool/plan` | Checks a statement and returns the columns it would return. Nothing runs. |
| `POST /api/provider-pool/query` | Runs a statement and returns its rows as JSON. |
| `GET /api/provider-pool/guidance` | Returns Parable's guidance on how to write SQL for these tables, as Markdown. |

The [API Reference](/api-reference/overview.md) has the full request and
response of each operation.

## Before you start

Send the two headers that every Workspace API request sends (see
[Authentication](/getting-started/authentication.md)):

* `Authorization: Bearer <token>`, with a Parable token or an MCP token. These
  operations only read, so an MCP token works. The API refuses a SQL token.
* `X-Tenant: <workspace-slug>`.

An MCP token works only while the Workspace allows external agents. People
who manage sign-in turn on **Allow external agents** in **Protection → Access
Tokens**. While it is off, the API refuses every MCP token with `403` and
`WA-PAT-006`.

The token's owner needs a role with one of these permissions:

| Operations | Permission | Roles that hold it |
| - | - | - |
| search, plan, query | `tenant.data.query.providers` | Provider Pools Reader, Data Access, Workspace Administrator |
| describe-table | `tenant.data.catalog` | Provider Pools Reader, Data Access, Workspace Administrator |
| guidance | Either permission | The same roles |

The examples on this page use three variables. Set `PARABLE_API_TOKEN` to a
Parable token or an MCP token, not to a SQL token:

```bash theme={null}
export PARABLE_API_URL="<the address Parable gives you>"
export PARABLE_API_TOKEN="<your Parable token or MCP token>"
export PARABLE_WORKSPACE="acme-corp"
```

## What these operations read

These operations read only the Provider Pools: `providers`,
`providers_quarantine`, and `providers_quality`. A statement that names
another pool, such as `workspace.users`, fails with `400` and
`Query references an unknown relation` (`QL-PL-002`). To read the
`artifacts`, `parables`, or `workspace` pools, use
[Flight SQL](/data.md#query-the-pools), or
[Arrow over HTTP](#arrow-over-http-for-every-pool) with a Parable token.

Provider Policies and source permissions narrow the rows of every statement
for every person, holders of All data access included. A script that runs
with a Workspace Administrator's token reads only what the Policies give that
administrator's roles. To read every row as a holder of All data access, use
Flight SQL with a SQL token. [Rows you can read](/data.md#rows-you-can-read)
explains both.

## Walk through a query

### 1. Find the table

Describe the data in plain words. `scope` narrows the search to a pool, a
Provider, or a table, with the same patterns as
[`search_available_data`](/data/search#find-tables-and-columns):

```bash theme={null}
curl -X POST "$PARABLE_API_URL/api/provider-pool/search" \
  -H "Authorization: Bearer $PARABLE_API_TOKEN" \
  -H "X-Tenant: $PARABLE_WORKSPACE" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "user last login time",
    "scope": "providers.google",
    "kind": null,
    "limit": 5,
    "offset": null,
    "cursor": null
  }'
```

```json Response (shortened) theme={null}
{
  "data": {
    "matches": [
      {
        "pool": "providers",
        "kind": "column",
        "tableRef": "providers.google.users",
        "columnName": "lastlogintime",
        "qualifiedName": "providers.google.users.lastlogintime",
        "provider": "Google Workspace",
        "description": "When the user last logged in (ISO 8601).",
        "dataType": "Timestamp(µs) (Temporal.DateTime)",
        "score": 14.57,
        "matchedBy": "keyword"
      }
    ],
    "providers": [],
    "limit": 5,
    "offset": 0
  },
  "meta": { "requestId": "..." }
}
```

Use `tableRef` in `FROM` and `columnName` in `SELECT`. `score` ranks the
results of one search. Compare it only within that search. `matchedBy` says
whether the words (`keyword`), the meaning (`semantic`), or `both` matched. A
search ranks at most 500 results. Page through them with `offset`.

To list tables instead of searching, send an empty `query` or `*`. With no
`scope`, the listing returns each Provider and its table count in
`providers`. With a scope such as `providers.google`, it returns that
Provider's tables. A listing page holds up to 100 tables or 40,000
characters. When there are more, send `nextCursor` back as `cursor`.

### 2. Describe the table

```bash theme={null}
curl -X POST "$PARABLE_API_URL/api/provider-pool/describe-table" \
  -H "Authorization: Bearer $PARABLE_API_TOKEN" \
  -H "X-Tenant: $PARABLE_WORKSPACE" \
  -H "Content-Type: application/json" \
  -d '{ "tableRef": "providers.google.users" }'
```

```json Response (shortened) theme={null}
{
  "data": {
    "table": {
      "catalog": "providers",
      "schemaName": "google",
      "logicalName": "users",
      "description": "Google Workspace users",
      "primaryKey": { "columns": ["id"] },
      "fields": [
        {
          "name": "lastlogintime",
          "arrowType": "Timestamp(µs)",
          "nullable": true,
          "scalarRef": "Temporal.DateTime",
          "system": false
        },
        {
          "name": "_parable_deleted",
          "arrowType": "Boolean",
          "nullable": false,
          "system": true
        }
      ]
    }
  },
  "meta": { "requestId": "..." }
}
```

`tableRef` is the exact name `pool.schema.table`. A name that the catalog does
not list returns `404` with `Pool table not found`. A field with no Parable
scalar has no `scalarRef`. `system: true` marks a column that Parable adds, such
as the [state columns](/data/providers.md#read-current-rows). Other fields are
not shown. They are the same as in [the catalog](/data.md#the-catalog).

### 3. Plan the query

`plan` checks a statement against the catalog and your access, and returns
the columns that it would return. It runs nothing and reads no rows:

```bash theme={null}
curl -X POST "$PARABLE_API_URL/api/provider-pool/plan" \
  -H "Authorization: Bearer $PARABLE_API_TOKEN" \
  -H "X-Tenant: $PARABLE_WORKSPACE" \
  -H "Content-Type: application/json" \
  -d '{
    "sql": "SELECT primaryemail, lastlogintime FROM providers.google.users WHERE suspended = $1 LIMIT 50",
    "parameters": [false]
  }'
```

```json Response (shortened) theme={null}
{
  "data": {
    "planned": true,
    "columns": [
      { "name": "primaryemail", "arrowType": "utf8", "scalarRef": "Contact.Email" },
      { "name": "lastlogintime", "arrowType": "timestamp[us]", "scalarRef": "Temporal.DateTime" }
    ],
    "elapsedMs": 368
  },
  "meta": { "requestId": "..." }
}
```

When Parable refuses a statement for a reason that you can correct, `plan`
still answers `200`. `planned` is `false`, and `refusal` has the error code
and a message that says what to change:

```json theme={null}
{
  "planned": false,
  "columns": [],
  "refusal": {
    "code": "QL-PL-002",
    "message": "Query references an unknown relation"
  }
}
```

### 4. Run the query

`query` takes the same body as `plan`:

```bash theme={null}
curl -X POST "$PARABLE_API_URL/api/provider-pool/query" \
  -H "Authorization: Bearer $PARABLE_API_TOKEN" \
  -H "X-Tenant: $PARABLE_WORKSPACE" \
  -H "Content-Type: application/json" \
  -d '{
    "sql": "SELECT primaryemail, lastlogintime FROM providers.google.users WHERE suspended = $1 ORDER BY lastlogintime DESC LIMIT 50",
    "parameters": [false]
  }'
```

```json Response (shortened) theme={null}
{
  "data": {
    "columns": [
      { "name": "primaryemail", "arrowType": "utf8", "scalarRef": "Contact.Email" },
      { "name": "lastlogintime", "arrowType": "timestamp[us]", "scalarRef": "Temporal.DateTime" }
    ],
    "rows": [
      ["jordan.rivera@example.com", "2026-10-20T14:03:11.000000"],
      ["sam.lee@example.com", "2026-10-19T09:41:52.000000"]
    ],
    "returnedRows": 2,
    "truncated": { "rows": false, "values": false, "capture": false },
    "elapsedMs": 458,
    "warnings": []
  },
  "meta": { "requestId": "..." }
}
```

Each row is a JSON array in the order of `columns`. `arrowType` says how to
read a value that arrives as text, such as a timestamp. Some values change form
so that JSON carries them exactly:

* A 64-bit integer outside ±(2<sup>53</sup> − 1), and every decimal, is a
  string.
* Binary data is base64 text.
* `NaN` and infinities are strings.

### Bind parameters

Write `$1`, `$2`, and so on in the SQL, and send the values in `parameters`
in the same order. Each value is a JSON string, number, boolean, or `null`.
Bind every value that comes from a person or a program. Do not build it into
the SQL text.

### See how Parable runs a statement

Send `EXPLAIN` followed by a query to `query`. It returns two rows,
`logical_plan` and `physical_plan`, in the columns `plan_type` and `plan`, and
runs nothing. [The plan](/data.md#see-the-plan-of-a-query) explains what the
plan shows.

## Results that are cut

A result is never larger than these limits. When a result reaches one, Parable
cuts it and says so. It never changes a value without saying so.

| `truncated` | Why | What to do |
| - | - | - |
| `rows` | The rows did not fit in 60,000 characters of JSON. `returnedRows` counts the whole rows that came back. | Select fewer columns, add a `LIMIT`, or aggregate. |
| `values` | A text or binary value was longer than 2,000 characters. It ends with `… [cut: N of M characters shown]`. | Select a part of the value, such as `substr(body, 1, 500)`. |
| `capture` | The result was larger than 3 MiB of Arrow before Parable converted it to JSON. | Add a `LIMIT` or aggregate. |

`warnings` says the same in plain sentences. A `LIMIT` in your SQL is the
best way to keep the whole result.

## Limits

| Limit | Value |
| - | - |
| Requests | 120 per minute for each operation. Past that, the API answers `429` with a `Retry-After` header. |
| Time | 30 seconds for each request. |
| Request body | 1 MB. The SQL can be up to 100,000 characters. |
| Result | 60,000 characters of JSON, 2,000 characters for one value, and 3 MiB of Arrow. |
| Listing | 100 tables or 40,000 characters a page. |
| Search | 1 to 100 results a page, and the top 500 results in all. |

## Errors

Errors use the Problem Details format with a `code`, as described in the
[API Reference](/api-reference/overview.md#errors).

| Status | Code | Meaning |
| - | - | - |
| `400` | The Query Layer's code, such as `QL-PL-002` or `QL-SQ-003` | Parable refused the SQL. The message says why, such as an unknown table or a statement that is not a `SELECT`. Correct the SQL. `plan` returns this as `refusal` instead. |
| `400` | `QL-SQ-010`, `QL-SQ-011` | The statement would pass a query's [AI call or cost limit](/data/ai.md). Do not retry. Call the function over fewer rows. |
| `403` | `WA-PAT-006` | The Workspace does not allow external agents, so it refuses MCP tokens. |
| `403` | `WA-PAT-005` and others | The token or its owner cannot make this request. See [Authentication](/getting-started/authentication.md#errors). |
| `404` | | `describe-table` found no table with that name. |
| `429` | | You sent more than 120 requests in a minute to one operation. Wait for `Retry-After`. |
| `429` | `QL-IN-008` | Too many queries are running in your Workspace. Try again in a moment. |
| `503` | `QL-IN-009`, `QL-IN-010` | Parable did not have the memory to run the query, or the model service that an AI function needs did not answer. Try again with backoff. |
| `504` | `QL-TO-001`, `QL-TO-002`, `QL-TO-003` | The query ran past its time budget. The message names the budget. Make the query read less. |
| `504` | `QL-TO-004` | `search_everything` could not read its matching records again in time. The message names the time limit. Run the search again. If it fails again, narrow its scope. |
| `504` | | The request ran past 30 seconds. |

## Read the query guidance

`GET /api/provider-pool/guidance` returns the guidance that Parable's own
agents read before they write SQL for these tables: how to find tables, read
Provider data, and write Plots. Each text has a `name`, a `title`, a
`description`, and Markdown `content`. `version` changes when the text
changes. The guidance is the same in every Workspace. Give it to your own
agent as context.

```bash theme={null}
curl "$PARABLE_API_URL/api/provider-pool/guidance" \
  -H "Authorization: Bearer $PARABLE_API_TOKEN" \
  -H "X-Tenant: $PARABLE_WORKSPACE"
```

## Check a query before you run it

`POST /api/parables/immediate-plot-query/actions/estimate` is a dry run. It
plans a statement exactly as running it would, and runs nothing. It tells you
how much data the statement reads, what that costs, and how many paid AI calls
it can make. It needs a Parable token. It refuses an MCP token with `403` and
`WA-PAT-005`. The owner needs `tenant.view` or `tenant.data.query.providers`.
The statement can read every pool that the owner's role can query, as over
Flight SQL.

```bash theme={null}
curl -X POST "$PARABLE_API_URL/api/parables/immediate-plot-query/actions/estimate" \
  -H "Authorization: Bearer $PARABLE_API_TOKEN" \
  -H "X-Tenant: $PARABLE_WORKSPACE" \
  -H "Content-Type: application/json" \
  -d '{
    "sql": "SELECT id, ponder(instruction => '\''Classify the issue.'\'', output => '\''Utf8'\'', inputs => named_struct('\''title'\'', title)) AS category FROM providers.linear.issues LIMIT 200",
    "parameters": null
  }'
```

```json Response (shortened) theme={null}
{
  "data": {
    "input": {
      "inputBytes": 48211456,
      "inputFileCount": 12,
      "inputRows": 18400,
      "unmeasuredLeaves": 0
    },
    "computeCost": {
      "byBytesRead": 0.0067,
      "centsPerGibRead": 0.15,
      "centsPerCpuHour": 6
    },
    "aiCallSites": [
      { "functionNames": ["ponder"], "maxCalls": 200 }
    ],
    "aiCallLimits": { "maxCalls": 1000000, "maxCostCents": 100000 }
  },
  "meta": { "requestId": "..." }
}
```

| Field | Meaning |
| - | - |
| `input` | The bytes, files, and rows that the statement scans. Parable counts them after it skips partitions and before any other filter, so they are an upper bound. They include rows that Provider Policies hide from you. `unmeasuredLeaves` above 0 means that some sources report no size, so the counts are low for them. |
| `computeCost` | The bytes priced at the platform rate, in US cents: 0.15 cents (1,500 micro-USD) for each GiB read. Nothing bills this figure. It is for you to compare queries. The response leaves out `byCpuTime` and `elapsedMs` until Parable can predict them for this kind of query. |
| `aiCallSites` | Each place in the plan that calls `ponder()`, `embed()`, or a Jev function, with `maxCalls`, the most paid calls it can make. A filter or a `LIMIT` often makes the real number much lower. |
| `aiCallLimits` | The [AI call and cost limits](/data/ai.md) that the statement would run under. |

The dry run refuses what running the statement would refuse, with the same
codes. It never refuses a statement for its size.

## Arrow over HTTP, for every pool

`POST /api/parables/immediate-plot-query` runs one statement and returns its
rows as an Arrow IPC stream (`application/vnd.apache.arrow.stream`). It takes
the same body as the dry run. Use it when you need Arrow, or a pool other than
the Provider Pools, without a Flight SQL client.

* It needs a Parable token. It refuses an MCP token and a SQL token.
* It reads every pool that the owner's role can query, under the same rules as
  Flight SQL. All data access applies.
* It stops a query after 30 seconds. Write a `LIMIT`: it caps no rows itself.
* The body is complete only when it ends with the Arrow end-of-stream marker.
  A body that ends without it failed. A refusal is a Problem Details error
  with the same codes as on this page.

```bash theme={null}
curl -X POST "$PARABLE_API_URL/api/parables/immediate-plot-query" \
  -H "Authorization: Bearer $PARABLE_API_TOKEN" \
  -H "X-Tenant: $PARABLE_WORKSPACE" \
  -H "Content-Type: application/json" \
  -d '{ "sql": "SELECT email, status FROM workspace.users LIMIT 100", "parameters": null }' \
  --output users.arrows
```

Read the file with any Arrow IPC stream reader, such as
`pyarrow.ipc.open_stream`.


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