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

# Custom Data API

> Manage tables and rows over HTTP.

Everything the UI does is available over the API. Base path `/core/custom-data`; see [Authentication](/getting-started/authentication) for tokens.

Scopes: `agents:read` to read, `agents:manage` to write, `custom_data:manage` to author stored queries.

## Tables

### List

```bash theme={null}
GET /core/custom-data/tables
```

### Get one

```bash theme={null}
GET /core/custom-data/tables/{name}
```

Includes `live_rows`, `tombstoned_rows` and `last_changed_at`.

### Create

```bash theme={null}
POST /core/custom-data/tables
```

```json theme={null}
{
  "name": "plans",
  "schema": {
    "type": "object",
    "required": ["plan_id"],
    "properties": {
      "plan_id":       { "type": "string", "x-key": true },
      "name":          { "type": "string", "description": "Customer-facing plan name" },
      "monthly_price": { "type": "number" },
      "region":        { "type": "string", "enum": ["EU", "US", "APAC"] },
      "available":     { "type": "boolean" }
    }
  }
}
```

The schema is a restricted subset of JSON Schema: a flat object, scalar types only (`string`, `number`, `integer`, `boolean`), an optional `format` of `date` or `date-time` on strings, and exactly one property marked `"x-key": true`, which must also be in `required`.

`409` if the name is taken.

### Update the schema

```bash theme={null}
PATCH /core/custom-data/tables/{name}
```

Same body. The row key cannot move once the table has rows — `400` if you try.

### Delete

```bash theme={null}
DELETE /core/custom-data/tables/{name}
```

Removes the table and every row for good. The one place data is truly erased.

## Rows

### Query

```bash theme={null}
POST /core/custom-data/tables/{name}/query
```

```json theme={null}
{
  "filters": [
    { "property": "region", "op": "eq", "value": "EU" },
    { "property": "monthly_price", "op": "lte", "value": 50 }
  ],
  "order": [{ "property": "monthly_price", "direction": "asc" }],
  "limit": 20
}
```

Operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `contains`, `starts_with`. Filters address declared properties only, and comparisons use the declared types — a price compares as a number, a date as a date.

A filter value that can't be read as its column's type matches nothing rather than failing the request, so one unusable value doesn't discard your other filters.

`limit` defaults to 20, maximum 500.

### Get one row

```bash theme={null}
GET /core/custom-data/tables/{name}/rows/{row_key}
```

```json theme={null}
{ "found": true, "record": { "plan_id": "pro-eu", "name": "Pro" } }
```

Always wrapped — `found` is `false` rather than a 404, so absent and deleted are indistinguishable to the caller.

### Replace everything

```bash theme={null}
POST /core/custom-data/tables/{name}/replace
```

```json theme={null}
{ "rows": [ { "plan_id": "pro-eu", "name": "Pro", "monthly_price": 49 } ] }
```

Full-state. Rows absent from `rows` are deleted. Returns counts:

```json theme={null}
{ "received": 900, "written": 3, "unchanged": 897, "tombstoned": 0,
  "live_before": 900, "live_after": 900, "warnings": [] }
```

Rows absent from `rows` are tombstoned, not erased: send a correct snapshot and they return.

### Add or update

```bash theme={null}
POST /core/custom-data/tables/{name}/upsert
```

Same body. Never deletes.

### Delete rows

```bash theme={null}
POST /core/custom-data/tables/{name}/delete-rows
```

```json theme={null}
{ "keys": ["legacy-eu", "legacy-us"] }
```

Marks them deleted. They return if the same keys appear in a later write.

### Claim a row

```bash theme={null}
POST /core/custom-data/tables/{name}/claim
```

```json theme={null}
{
  "filters": [{ "property": "used", "op": "eq", "value": false }],
  "order":   [{ "property": "pct", "direction": "asc" }],
  "patch":   { "used": true }
}
```

Takes one matching row, applies `patch`, returns it. Concurrent callers never receive the same row. `{ "found": false }` when nothing is available — a `200`, not an error. `patch` may only set declared properties and may not touch the row key.

## Stored queries

```bash theme={null}
GET    /core/custom-data/tables/{name}/queries
PUT    /core/custom-data/tables/{name}/queries/{query_name}
POST   /core/custom-data/tables/{name}/queries/dry-run
POST   /core/custom-data/tables/{name}/queries/{query_name}/enabled
POST   /core/custom-data/tables/{name}/queries/{query_name}/run
DELETE /core/custom-data/tables/{name}/queries/{query_name}
```

Authoring (`PUT`, `dry-run`, `enabled`, `DELETE`) needs `custom_data:manage`. Listing and running need only the table's read scope.

Saving:

```json theme={null}
{
  "sql": "SELECT s.name FROM stores s, args a WHERE s.region = a.region",
  "params": [{ "name": "region", "type": "string" }],
  "row_limit": 5
}
```

A query that fails validation is still saved, with `status: "broken"` and a `validation_error`. Only `validated` queries can be enabled.

Running:

```json theme={null}
{ "args": { "region": "EU" } }
```

Arguments are bound as data, never inserted into the SQL text.

## Tool schema

```bash theme={null}
GET /core/custom-data/tables/{name}/tool-schema
```

Returns every agent tool the table produces — the generated lookup and search pair, plus one per enabled stored query — as complete tool definitions ready to drop into an agent's `tools`.

## Errors

| Status | Meaning                                                    |
| ------ | ---------------------------------------------------------- |
| `400`  | Malformed request, or a schema/row that breaks the profile |
| `403`  | Missing scope                                              |
| `404`  | No such table, row or query                                |
| `409`  | A table with that name already exists                      |
| `503`  | The read couldn't complete — retryable                     |

`503` is worth handling separately from `500`: the request was well formed and the same request may succeed on retry.
