YAML Views¶
Views are pre-approved, named SQL queries defined in YAML files. They let you publish a curated set of data access patterns — with typed parameters and descriptions — without exposing raw SQL to API consumers.
Use views to:
- Give AI tools and dashboards clean, stable query interfaces
- Enforce read patterns (consumers call a view name, not arbitrary SQL)
- Document data access for compliance reviewers
Enabling views¶
Set TDB_VIEWS_DIR to a directory containing your view YAML files. Views are loaded at startup. If the variable is not set, the views feature is disabled.
View definition format¶
Each view is a separate .yml or .yaml file. One file = one view.
Minimal view (no parameters):
name: daily_signups
description: "New user signups grouped by day"
source: analytics_db
sql: |
SELECT DATE(created_at) AS day, COUNT(*) AS signups
FROM users
GROUP BY day
ORDER BY day DESC
Parameterised view:
name: orders_by_status
description: "Orders filtered by status and capped at a row limit"
source: orders_db
sql: "SELECT * FROM orders WHERE status = :status LIMIT :limit"
parameters:
- name: status
type: string
description: "Order status (pending, shipped, delivered)"
required: true
- name: limit
type: integer
description: "Maximum number of rows to return"
required: false
default: 50
Fields¶
| Field | Required | Description |
|---|---|---|
name |
Yes | View name (1–128 chars). Must be unique across all view files. |
description |
No | Human-readable description shown in API responses |
source |
Yes | Registered source name (not ID) to execute the query against |
sql |
Yes | SQL query template. Use :param_name placeholders for parameters. |
parameters |
No | List of parameter definitions (see below) |
Parameter fields¶
| Field | Required | Description |
|---|---|---|
name |
Yes | Parameter name (1–64 chars). Must match a :placeholder in the SQL. |
type |
No | One of string, integer, float, boolean. Default: string. |
description |
No | Human-readable description |
required |
No | If true, callers must supply a value. Default: false. |
default |
No | Default value used when the parameter is omitted. Ignored if required: true. |
Parameter types and SQL safety¶
Parameter values are type-checked and safely embedded before execution. TDB does not use string interpolation — each type produces a validated SQL literal:
| Type | Input | SQL output |
|---|---|---|
string |
"O'Brien" |
'O''Brien' (single quotes escaped) |
string on MySQL / Snowflake |
"C:\\dir" |
'C:\\dir' (backslashes escaped too, from 0.12.0) |
integer |
"42" or 42 |
42 |
float |
"3.14" or 3.14 |
3.14 |
boolean |
"true", "1", "yes" |
1 |
boolean |
"false", "0", "no" |
0 |
Supplying a value that cannot be coerced to the declared type returns HTTP 400.
MySQL and Snowflake treat a backslash inside a string literal as an escape character, so on those sources backslashes are doubled as well as quotes and the value arrives unchanged. Before enterprise 0.12.0 only quotes were escaped, and a string parameter containing a backslash could end the literal early — upgrade if you run views with string parameters against either engine.
REST endpoints¶
All view endpoints require Bearer auth (any valid API key with read role or above).
List views¶
Returns all view definitions loaded from TDB_VIEWS_DIR.
Response:
[
{
"name": "daily_signups",
"description": "New user signups grouped by day",
"source": "analytics_db",
"parameters": []
},
{
"name": "orders_by_status",
"description": "Orders filtered by status and capped at a row limit",
"source": "orders_db",
"parameters": [
{"name": "status", "type": "string", "description": "Order status (pending, shipped, delivered)", "required": true, "default": null},
{"name": "limit", "type": "integer", "description": "Maximum number of rows to return", "required": false, "default": 50}
]
}
]
Get a view¶
Returns the definition for a single view. Returns HTTP 404 if not found.
Run a view¶
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
parameters |
object | No | Key-value pairs for named parameters. |
limit |
integer | No | Max rows to return. Default: 1,000, or TDB_MAX_ROWS if lower. At most TDB_MAX_ROWS; a larger value is refused with 400 and audited limit_exceeds_max (from 0.16.0; before, it was a fixed 1–1000 whatever TDB_MAX_ROWS said). |
page_key, cursor |
array of strings, string | No | From 0.31.0: read the view page by page. |
Response:
{
"view": "orders_by_status",
"source": "orders_db",
"columns": ["id", "customer_id", "status", "amount"],
"rows": [
{"id": 1, "customer_id": 42, "status": "pending", "amount": 99.99}
],
"rows_returned": 1,
"truncated": false,
"executed_at": "2026-05-22T09:01:23.456789Z"
}
curl -X POST http://localhost:8000/v1/views/orders_by_status/run \
-H "Authorization: Bearer <KEY>" \
-H "Content-Type: application/json" \
-d '{"parameters": {"status": "pending"}, "limit": 100}'
MCP tool¶
Views are also callable via the MCP list_views and run_view tools when using Claude Desktop, Cursor, or any MCP client.
list_views() → returns all view names and descriptions
run_view(name, params) → executes the named view
A key's allowed_tools scope applies to both: the MCP tools above, and — from enterprise 0.12.0 — the REST routes on this page (list_views for listing and reading views, run_view for running one). A key scoped to ["run_view"] can run views and cannot send its own SQL. See RBAC → Restricting tool access.
Error responses¶
| Scenario | HTTP status | Detail |
|---|---|---|
| View not found | 404 | View 'name' not found. |
| Missing required parameter | 400 | Missing required parameters: ['status'] |
| Unknown parameter supplied | 400 | Unknown parameters: ['typo'] |
| Type coercion failure | 400 | Parameter 'limit' expects integer, got 'abc' |
| Source not registered | 400 | View 'name' references source 'x' which is not registered. |
| Source rejected the view's SQL | 400 | View failed: <error from the source> |
| Source failed while running it | 500 | View execution failed: <error from the source> |
The first four 400s are caused by the caller — a missing, unknown or mistyped
parameter. View failed is usually caused by the view definition: the SQL in
the YAML was accepted at load time but rejected by the source when it ran (an
unknown column, a syntax error, a value that does not convert), so it surfaces on
whoever calls the view rather than on whoever wrote it. Before enterprise 0.20.3
this was a 500. A parser error in the sql: field is the common case — check the view against
the parameter syntax (:name, not {{ name }})
and run the same SQL through POST /v1/query to see the source's own
error message.
Example: full setup¶
# 1. Create view files
mkdir /etc/tdb/views
cat > /etc/tdb/views/revenue_by_month.yml << 'EOF'
name: revenue_by_month
description: "Monthly revenue totals, optionally filtered by year"
source: orders_db
sql: |
SELECT strftime('%Y-%m', order_date) AS month, SUM(amount) AS total
FROM orders
WHERE (:year = 0 OR strftime('%Y', order_date) = :year)
GROUP BY month
ORDER BY month DESC
parameters:
- name: year
type: string
description: "4-digit year to filter by, or '0' for all years"
required: false
default: "0"
EOF
# 2. Start TDB with the views directory
TDB_VIEWS_DIR=/etc/tdb/views tdb serve
# 3. List views
curl -H "Authorization: Bearer <KEY>" http://localhost:8000/v1/views
# 4. Run a view
curl -X POST http://localhost:8000/v1/views/revenue_by_month/run \
-H "Authorization: Bearer <KEY>" \
-H "Content-Type: application/json" \
-d '{"parameters": {"year": "2026"}}'
Reload behavior¶
Views are loaded once at startup. To add, remove, or modify view files, restart TDB. There is no hot-reload — a restart ensures the audit log records which view definition was active at query time.