Query
Rekor provides two ways to query data: SQL queries for agents and advanced use cases, and a Filter DSL for the frontend list endpoint and trigger conditions.
SQL Query
Execute read-only SQL queries against workspace data. Supports filtering, aggregation, JOINs across collections, CTEs, and array operations.
Endpoint
POST /v1/{workspace_id}/sql
Request
{
"query": "SELECT ... FROM records FINAL WHERE workspace_id = {workspace_id:String} AND ...",
"params": { "custom_param": "value" }
}
Response
{
"data": [ { "column": "value", ... }, ... ],
"meta": { "rows": 100, "has_more": false }
}
Rules
- Always include
workspace_id = {workspace_id:String}— the placeholder is auto-filled with the workspace from the URL - Always include
deleted = falseto exclude soft-deleted records - Always add
FINALafter table names (e.g.,FROM records FINAL) - Read-only — only
SELECTstatements are allowed - Max 1000 rows per query (automatically enforced)
- 10 second timeout, 512MB memory limit
Available tables
| Table | Description |
|---|---|
records | All records across collections. Filter by collection = 'name'. |
relationships | All relationships. Filter by rel_type = 'name'. |
collections | Collection metadata and schemas. |
workspaces | Workspace metadata. |
operations_log | Audit log of all write operations. |
Accessing JSON fields
Record data is stored in a data column (JSON type). Access fields using subcolumn syntax:
-- String field
data.status.:String
-- Numeric field (use CAST for type-safe conversion)
CAST(data.amount, 'Float64')
-- Date filtering
CAST(data.due_at.:String, 'Date') < today()
Working with arrays
Arrays embedded in JSON can be queried using subcolumn syntax and array functions:
-- Sum values from an array of objects
arraySum(CAST(data.line_items[].amount, 'Array(Float64)'))
-- Explode array elements into rows
SELECT item.description.:String, CAST(item.amount, 'Float64')
FROM records FINAL
ARRAY JOIN data.line_items[] as item
WHERE workspace_id = {workspace_id:String}
AND collection = 'invoices'
AND deleted = false
CTEs and JOINs
Use Common Table Expressions to join data across tables. This is useful for reconciliation queries that combine records with relationship metadata:
WITH
inv AS (
SELECT id, data.invoice_number.:String as invoice_number,
arraySum(CAST(data.line_items[].amount, 'Array(Float64)')) as total
FROM records FINAL
WHERE workspace_id = {workspace_id:String}
AND collection = 'invoices' AND deleted = false
),
pay AS (
SELECT target_id,
sum(CAST(data.allocated, 'Float64')) as total_paid
FROM relationships FINAL
WHERE workspace_id = {workspace_id:String}
AND rel_type = 'payment_for' AND deleted = false
GROUP BY target_id
)
SELECT inv.invoice_number, inv.total,
coalesce(pay.total_paid, 0) as paid,
inv.total - coalesce(pay.total_paid, 0) as balance
FROM inv
LEFT JOIN pay ON pay.target_id = inv.id
ORDER BY balance DESC
Cross-Workspace Query
Query data across all workspaces in an account. Useful for dashboards, reporting, and analytics that span multiple workspaces.
Endpoint
POST /v1/accounts/{account_id}/sql
Request
{
"query": "SELECT workspace_id, collection, count() as record_count FROM records FINAL WHERE account_id = {account_id:String} AND deleted = false GROUP BY workspace_id, collection ORDER BY record_count DESC"
}
Uses {account_id:String} instead of {workspace_id:String}. The token must have read:records permission. Results are scoped to the token's account.
Examples
-- Record counts across all workspaces
SELECT workspace_id, count() as total
FROM records FINAL
WHERE account_id = {account_id:String} AND deleted = false
GROUP BY workspace_id
-- Search for a record across all workspaces
SELECT workspace_id, collection, id, data.name.:String as name
FROM records FINAL
WHERE account_id = {account_id:String}
AND deleted = false
AND data.name.:String ILIKE '%acme%'
Filter DSL
JSON filter expressions used by the frontend list endpoint (GET /v1/{workspace}/records/{collection}?filter=...) and trigger conditions.
Simple filter
{
"field": "data.status",
"op": "eq",
"value": "active"
}
Compound filter
{
"and": [
{ "field": "data.status", "op": "eq", "value": "issued" },
{ "field": "data.amount", "op": "gt", "value": 5000 }
]
}
Operators
| Operator | Description | Example |
|---|---|---|
eq | Equal | {"field":"data.status","op":"eq","value":"active"} |
neq | Not equal | {"field":"data.status","op":"neq","value":"deleted"} |
gt | Greater than | {"field":"data.amount","op":"gt","value":100} |
gte | Greater than or equal | {"field":"data.amount","op":"gte","value":100} |
lt | Less than | {"field":"data.amount","op":"lt","value":100} |
lte | Less than or equal | {"field":"data.amount","op":"lte","value":100} |
in | In array | {"field":"data.status","op":"in","value":["active","pending"]} |
not_in | Not in array | {"field":"data.status","op":"not_in","value":["deleted"]} |
like | Pattern match (case-sensitive) | {"field":"data.name","op":"like","value":"%Jane%"} |
ilike | Pattern match (case-insensitive) | {"field":"data.name","op":"ilike","value":"%jane%"} |
is_null | Is null | {"field":"data.email","op":"is_null"} |
is_not_null | Is not null | {"field":"data.email","op":"is_not_null"} |
has | Array contains | {"field":"data.tags","op":"has","value":"urgent"} |
Sorting
{
"sort": [
{ "field": "data.created_at", "direction": "desc" },
{ "field": "data.name", "direction": "asc" }
]
}
Pagination
Use limit and offset query parameters:
GET /v1/{workspace}/records/{collection}?limit=20&offset=40
Field selection
Return a subset of fields with the fields parameter:
GET /v1/{workspace}/records/{collection}?fields=id,data.name,data.status