DashboardSign inStart your trial

In this guide · OperateClose guide

Query workspace data

Use POST /query to count, sum, average, and bucket workspace data for dashboards, with worked examples for pipeline, run, and approval metrics.

POST /api/v1/query returns aggregates over one dataset: counts, sums, averages, percentiles, grouped by field and bucketed by time. It’s built for dashboards and reports. It isn’t SQL: every query is a JSON description that Rills checks against an allowlist of datasets, fields, and operations before anything runs.

#Request shape

{
  "dataset": "workflowExecutions",
  "resourceId": "00000000-0000-4000-8000-000000000000",
  "timeRange": { "from": "2026-01-01T00:00:00Z", "to": "2026-01-08T00:00:00Z" },
  "timeBucket": { "field": "createdAt", "interval": "day" },
  "filters": [{ "field": "status", "op": "eq", "value": "failed" }],
  "groupBy": [],
  "measures": [{ "alias": "failedRuns", "fn": "count" }],
  "orderBy": [{ "field": "bucket", "direction": "asc" }],
  "limit": 1000
}
KeyRequiredDescription
datasetYesOne of the datasets below.
resourceIdDependsTable ID for tableRows and form ID for formSubmissions (required). Optional workflow ID for workflowExecutions. Not accepted for other datasets.
timeRangeDependsfrom (inclusive) and to (exclusive) as ISO 8601 timestamps with a zone, at most 366 days apart.
timeBucketNofield (a date field) and interval: minute, hour, day, week, or month.
filtersNoUp to 10 conditions, all of which must match.
groupByNoUp to 3 distinct field names.
measuresYes1 to 10 aggregates, each with a unique alias.
orderByNoUp to 2 sorts on bucket, a grouped field, or a measure alias.
limitNoMaximum rows returned, 1 to 1,000. Default 1,000.

The body must be application/json and at most 64 KiB. Unknown keys are rejected.

#Datasets and fields

DatasetRequired scopesFields
tableRowstables:read, payloads:readcreatedAt, updatedAt, and your table’s fields
formSubmissionsforms:read, payloads:readcreatedAt, updatedAt, and the form’s table fields
workflowExecutionsworkflows:readcreatedAt, workflowId, workflowName, status, durationMs
approvalsapprovals:readcreatedAt, decidedAt, workflowId, status, decisionKind, priority, source, timeToDecisionMs, confidenceScore
decisionRecordsrecords:readcreatedAt, category, action, resourceType, actorType
agentConversationsagents:readcreatedAt, updatedAt, agentId, status
filesfiles:readcreatedAt, source, contentType, sizeBytes

Every listed field can be filtered, grouped, and measured. Only date fields (createdAt, updatedAt, decidedAt, and date-type table fields) can be bucketed.

Useful values:

  • workflowExecutions.status: pending, running, success, failed, paused_quota, paused_billing_lapse.
  • approvals.status: pending, committing, approved, rejected, expired, auto_approved. committing means decided but still undoable until the decision’s effectiveAt.
  • approvals.decisionKind: approved, rejected, edited, auto_approved; null until decided.
  • approvals.priority: low, normal, high, urgent.
  • decisionRecords.actorType: user, system, api_token.
  • files.source: form or workspace.
  • durationMs and timeToDecisionMs are integer milliseconds; they’re null while a run is unfinished or an approval is undecided.

agentConversations reports counts and status only. Message content isn’t queryable.

#Table and form fields

For tableRows and formSubmissions, refer to your own fields by slug. GET /tables/{tableId} lists each field’s slug and fieldType. Field types map to query types:

  • number fields are numeric;
  • date fields are calendar dates and can be bucketed;
  • bool fields are true/false;
  • other field types are treated as text;
  • json and file fields and archived fields can’t be queried.

A formSubmissions query covers rows that the form wrote into its table.

#Filters and measures

OperatorWorks onValue
eq, neAny fieldOne value
in, notInAny fieldArray of 1 to 100 values
lt, lte, gt, gteNumber and date fieldsOne value
isNull, isNotNullAny fieldOmit value

Values must match the field’s type: strings for text and IDs, numbers for numeric fields, true or false for booleans, and ISO 8601 strings for dates. Strings are limited to 500 characters.

MeasureFieldNotes
countOmit fieldCounts matching rows.
countDistinctAny fieldCounts distinct non-null values.
sum, avgNumeric fieldNulls are skipped.
min, maxNumeric or date fieldNulls are skipped.
percentileNumeric fieldAdd percentile strictly between 0 and 1, for example 0.95.

Aliases must start with a lowercase letter, use letters, digits, or underscores, and can’t be bucket or a grouped field name. A table field named bucket can’t be grouped in a query that also sets timeBucket.

#Time buckets

Buckets are ISO 8601 UTC timestamps at the start of each interval, for example 2026-01-05T00:00:00.000Z. Days start at 00:00 UTC, weeks start Monday 00:00 UTC, and months start on the 1st. Workspace time zones aren’t applied.

When timeBucket is set, timeRange filters on the bucketed field. Without a bucket, timeRange filters on createdAt. So bucketing approvals on decidedAt counts decisions made in the range, regardless of when the request was created.

#Rules

Some shapes are rejected with 400 QUERY_TOO_EXPENSIVE before anything runs:

  • Every bucketed query needs timeRange.
  • Fixed datasets need timeRange (workflowExecutions, approvals, decisionRecords, agentConversations, files). The one exception is a plain total of pending approvals: a single count with only the filter status eq pending.
  • Tables and forms need timeRange for grouping, bucketing, non-count measures, and range or null filters. Without a range, you can run a plain count whose filters are eq or in on text fields. Tables larger than 64 MiB always need a range.
  • from must be earlier than to, at most 366 days apart.
  • Every field must exist in the dataset, every operator and measure must suit the field’s type, and orderBy can only name bucket, a grouped field, or an alias.

Queries are cancelled after 5 seconds and return 503 QUERY_TIMEOUT with Retry-After: 30. Narrow the range or grouping before retrying. Each query costs 10 request units; see rate limits.

#Response

{
  "data": [
    { "bucket": "2026-01-05T00:00:00.000Z", "status": "success", "runs": 42 },
    { "bucket": "2026-01-05T00:00:00.000Z", "status": "failed", "runs": 3 }
  ],
  "meta": { "dataset": "workflowExecutions", "generatedAt": "2026-01-08T09:15:02.481Z" }
}

Each row is flat: bucket when you bucket, then each grouped field, then each measure alias. Without orderBy, grouped rows are ordered by bucket and then by grouped fields.

Grouping by a number field (such as durationMs or a number table field) returns the key as an exact decimal string, for example "0.1", so two different stored values never share a key. Those keys sort as text; order by a measure if you need a different order. Measures are always JSON numbers.

  • A scalar count with no matches returns one row with 0.
  • A grouped or bucketed query with no matches returns "data": []. Buckets with no rows are omitted, not zero-filled.
  • The response is capped at 2 MiB (413 RESPONSE_TOO_LARGE).

#Examples

These bodies match the examples in the OpenAPI document. Replace 00000000-0000-4000-8000-000000000000 with your table, form, or workflow ID, and set timeRange to the period you want.

#Leads not yet contacted

Count rows in a leads table whose contactStatus text field is not_contacted. A plain text-equality count can omit timeRange to count all time. Requires tables:read and payloads:read.

{
  "dataset": "tableRows",
  "resourceId": "00000000-0000-4000-8000-000000000000",
  "filters": [{ "field": "contactStatus", "op": "eq", "value": "not_contacted" }],
  "measures": [{ "alias": "leadCount", "fn": "count" }]
}

Returns [{ "leadCount": 128 }].

#Daily runs by status for one workflow

Requires workflows:read. resourceId is the workflow ID.

{
  "dataset": "workflowExecutions",
  "resourceId": "00000000-0000-4000-8000-000000000000",
  "timeRange": { "from": "2026-01-01T00:00:00Z", "to": "2026-01-08T00:00:00Z" },
  "timeBucket": { "field": "createdAt", "interval": "day" },
  "groupBy": ["status"],
  "measures": [{ "alias": "runs", "fn": "count" }],
  "orderBy": [{ "field": "bucket", "direction": "asc" }]
}

#Average and p95 run duration by day

Requires workflows:read. Add resourceId to limit it to one workflow. Unfinished runs have no duration and are skipped.

{
  "dataset": "workflowExecutions",
  "timeRange": { "from": "2026-01-01T00:00:00Z", "to": "2026-01-08T00:00:00Z" },
  "timeBucket": { "field": "createdAt", "interval": "day" },
  "measures": [
    { "alias": "averageMs", "fn": "avg", "field": "durationMs" },
    { "alias": "p95Ms", "fn": "percentile", "field": "durationMs", "percentile": 0.95 }
  ],
  "orderBy": [{ "field": "bucket", "direction": "asc" }]
}

#Pending approvals by priority

Requires approvals:read. Grouping needs a range, and the range applies to createdAt, so make it at least as long as your oldest pending request can be. Standalone requests expire after at most 30 days.

{
  "dataset": "approvals",
  "timeRange": { "from": "2026-01-01T00:00:00Z", "to": "2026-01-08T00:00:00Z" },
  "filters": [{ "field": "status", "op": "eq", "value": "pending" }],
  "groupBy": ["priority"],
  "measures": [{ "alias": "pending", "fn": "count" }]
}

For one total across all time, drop timeRange and groupBy:

{
  "dataset": "approvals",
  "filters": [{ "field": "status", "op": "eq", "value": "pending" }],
  "measures": [{ "alias": "pending", "fn": "count" }]
}

#Approval decisions by day and kind

Requires approvals:read. Bucketing on decidedAt puts each decision on the day it was made.

{
  "dataset": "approvals",
  "timeRange": { "from": "2026-01-01T00:00:00Z", "to": "2026-01-08T00:00:00Z" },
  "timeBucket": { "field": "decidedAt", "interval": "day" },
  "filters": [{ "field": "decidedAt", "op": "isNotNull" }],
  "groupBy": ["decisionKind"],
  "measures": [{ "alias": "decisions", "fn": "count" }]
}

#Form submissions by day

Requires forms:read and payloads:read. resourceId is the form ID.

{
  "dataset": "formSubmissions",
  "resourceId": "00000000-0000-4000-8000-000000000000",
  "timeRange": { "from": "2026-01-01T00:00:00Z", "to": "2026-01-08T00:00:00Z" },
  "timeBucket": { "field": "createdAt", "interval": "day" },
  "measures": [{ "alias": "submissions", "fn": "count" }]
}

#Next steps

  • Grafana: turn these queries into dashboard panels
  • MCP: ask an AI client the same questions through aggregate tools