DashboardSign inStart your trial

In this guide · OperateClose guide

Pagination, errors & rate limits

Cursor pagination, filters, error codes, request budgets, refresh guidance, and idempotent actions for the Rills API.

#Pagination

List endpoints return a page of results and an opaque cursor:

{
  "data": [{ "id": "…" }],
  "page": { "nextCursor": "8c1f…" }
}

Pass nextCursor back as cursor to fetch the next page. When nextCursor is null, you’ve reached the end. Keep every other parameter the same between pages.

ParameterDefaultMaximum
limit on metadata lists50250
limit on payload lists (row values, submissions, step data, agent memory, feedback, eval runs, conversation events)50100

Ordering is deterministic, so pages don’t skip or repeat items. Treat cursors as opaque: don’t build or edit them.

Lists return items in a stable ID order by default. Approvals, executions, and table and form row lists also accept sort=-createdAt for newest first, and approvals accept sort=-decidedAt for the most recent decisions first. Cursors work the same way in either order.

#Filters

Most lists accept ISO 8601 timestamp filters: createdAfter and createdBefore, plus updatedAfter and updatedBefore where the resource tracks updates. Lists also accept filters specific to that resource. For example, executions filter by workflowId, workflowVersionId, and status, and approvals by status, priority, source, workflowId, executionId, assignedTo, decisionKind, decidedAfter, and decidedBefore. The approval status filter also accepts decided, which matches approved, rejected, auto-approved, and committing requests. A committing request has a decision that the reviewer can still undo until its effectiveAt, so it is not final: act on an approval only once its status is approved, rejected, auto_approved, or expired. Approval workflowId, executionId, and decisionKind take up to 100 comma-separated values and match any of them. The OpenAPI document lists every filter for every endpoint.

Unknown or repeated query parameters return 400 VALIDATION_ERROR instead of being ignored, so a typo can’t silently widen your results.

#Expansions

Some endpoints can include related data in the same response with include:

  • include=payload on GET /approvals and GET /approvals/{id} adds each approval’s card as payload: the proposed action, context, confidence reasoning, workflow name, and decision. It needs payloads:read, and pages hold at most 100 approvals.
  • include=stats on GET /workflows and GET /workflows/{id} adds stats: trigger type, pending approvals, runs per UTC day for the last 14 days, and success, approval, and confidence rates. It needs only workflows:read. The numbers come from the same data as the workflowExecutions and approvals datasets in /query.

#Filtering on values

Metadata row and submission lists filter only on envelope fields such as timestamps and source form. Filtering on your own column values requires payloads:read, through the payload lists or /query.

#Errors

Every error has the same shape:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Unknown or invalid query parameter",
    "requestId": "5b0e…"
  }
}

Branch on code, not message. Messages are for people and may change.

A form submission that fails validation adds fieldErrors, one entry per invalid field, where path is the field’s slug:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Please fix the highlighted fields.",
    "requestId": "5b0e…",
    "fieldErrors": [{ "path": "email", "message": "Invalid email address" }]
  }
}
StatusCodeMeaningWhat to do
400VALIDATION_ERRORMalformed body, unknown parameter, missing Idempotency-Key, or a body over its size limit.Fix the request.
400QUERY_TOO_EXPENSIVEA /query shape outside the allowed rules, such as a missing time range or an unknown field.Add a time range or narrow the query. See query rules.
402QUOTA_EXCEEDED or SPENDING_CAP_EXCEEDEDA form submission would exceed the workspace’s storage.Free up storage or raise the spending cap.
401UNAUTHORIZEDMissing, invalid, expired, or revoked credential.Replace the key or reconnect the client.
403FORBIDDENValid credential without a required scope.Grant the scopes named in the WWW-Authenticate header.
404NOT_FOUNDThe resource doesn’t exist, is archived, or belongs to another workspace.Check the ID and the credential’s workspace.
409IDEMPOTENCY_CONFLICTAn Idempotency-Key was reused with a different body.Use a new key for a new request.
409DECISION_COMMITTEDAn approval decision’s undo window has closed.None: the workflow has received the decision.
413RESPONSE_TOO_LARGEThe response would exceed 2 MiB.Lower limit or narrow filters.
429RATE_LIMITEDA request budget or concurrency limit was hit.Wait for Retry-After, then slow down.
503QUERY_TIMEOUTA /query ran past its 5-second limit and was cancelled.Wait Retry-After (30 seconds), then narrow the time range, filters, or grouping.
503IDEMPOTENCY_IN_PROGRESS or IDEMPOTENCY_UNAVAILABLEThe first request with this key hasn’t finished yet.Retry with the same key and body after Retry-After.
500INTERNAL_ERRORSomething failed on our side.Retry with backoff; contact support with the requestId if it persists.

A 403 from a scope check includes the scopes it needed:

WWW-Authenticate: Bearer error="insufficient_scope", scope="tables:read payloads:read"

The API returns 404 rather than 403 for IDs in another workspace, so it never confirms that someone else’s resource exists.

#Rate limits

Limits keep shared infrastructure responsive for every workspace. They aren’t a pricing tier, and API reads never use action credits.

LimitValue
Requests per IP address, before authentication900 per minute
Request units per credential1,200 per minute
Request units per workspace, across all credentials6,000 per minute
Concurrent /query requests per credential4
Concurrent /query requests per workspace20

Each request costs units:

RequestUnits
Metadata list or detail1
Payload list page or payload detail5
POST /query10
Any action in Actions and idempotency10

MCP aggregate tools draw from the same credential and workspace budgets as /query.

Successful responses report the credential’s budget:

HeaderMeaning
RateLimit-LimitUnits available to this credential per minute.
RateLimit-RemainingUnits left in the current minute.
RateLimit-ResetWhen the current window resets, as Unix time in seconds.

A 429 adds Retry-After in seconds. Honor it; retrying sooner fails again and spends the next window’s headroom.

#Dashboard refresh

You choose your dashboard’s refresh interval, subject to these limits; there is no unlimited refresh rate. Each query panel costs 10 units per refresh. Ten panels refreshing every 10 seconds use 600 units per minute, half of one credential’s budget, which leaves room for retries and manual reloads.

Start at 30 seconds. For multi-panel dashboards, stay at 10 seconds or slower, and watch RateLimit-Remaining before going faster. Give separate dashboards separate keys so one busy screen can’t starve another.

#Actions and idempotency

These endpoints change state:

EndpointScopeSuccess
POST /workflows/{workflowId}/runsworkflows:execute202 with { "data": { "executionId", "status": "pending" } }
POST /approval-requestsapprovals:request201 with { "data": { "approvalId", "status" } }
POST /approvals/{approvalId}/decisionapprovals:decide200 with { "data": { "approvalId", "decisionId", "status": "committing", "effectiveAt" } }
POST /approvals/{approvalId}/decision/undoapprovals:decide200 with { "data": { "approvalId", "status": "pending" } }
POST /forms/{formId}/submissionsforms:submit201 with { "data": { "rowId" } }
POST /optimization-suggestions/{suggestionId}/decisionoptimization:decide200 with { "data": { "suggestionId", "status" } }
POST /optimization-suggestions/{suggestionId}/revertoptimization:decide200 with { "data": { "suggestionId", "reverted": true } }
POST /optimization-suggestions/{suggestionId}/restoreoptimization:decide200 with { "data": { "suggestionId", "status": "pending" } }

An approval decision stays undoable until effectiveAt, 10 seconds after it is recorded. Only the reviewer who decided can undo it. After effectiveAt the workflow has the decision, and undo returns 409 DECISION_COMMITTED. Revert restores the configuration an accepted suggestion replaced, at any time. Restore returns a dismissed suggestion to pending.

Starting a run, requesting an approval, deciding an approval, submitting a form and deciding a suggestion require an Idempotency-Key header: 1 to 255 printable ASCII characters with no spaces. A UUID per logical operation works well. Rills keeps each key for 24 hours:

  • Sending the same key and body again returns the original status and body with Idempotency-Replayed: true. Nothing runs twice.
  • Sending the same key with a different body returns 409 IDEMPOTENCY_CONFLICT.
  • If the first request is still running, the retry waits briefly, then returns 503 with Retry-After so you can try again with the same key.

Start a workflow, optionally choosing a trigger and passing trigger data:

curl --fail-with-body \
  --request POST \
  --header "Authorization: Bearer $RILLS_API_KEY" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: $(uuidgen)" \
  --data '{"triggerPayload": {"email": "jordan@example.com"}}' \
  "https://rills.ai/api/v1/workflows/<workflow-id>/runs"

Submit a proposal for human review:

curl --fail-with-body \
  --request POST \
  --header "Authorization: Bearer $RILLS_API_KEY" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: $(uuidgen)" \
  --data '{"title": "Send renewal offer to Acme", "actionType": "send_email", "parameters": {"to": "buyer@acme.example"}, "priority": "high"}' \
  "https://rills.ai/api/v1/approval-requests"

Approval requests take title (up to 200 characters), actionType (up to 80), optional parameters and context objects (128 KiB combined), priority (low, normal, high, or urgent; default normal), and expiresInSeconds (60 seconds to 30 days; default 7 days). Approvals are free. A started workflow’s actions are billed as usual.

Action bodies are capped at 256 KiB, 20 levels of nesting, 10,000 total values, 1,000 items per array, and 32 KiB per string. A request over any cap returns 400 VALIDATION_ERROR and creates nothing.