Quorum: agent control plane
Quorum governs agents that you run on your own infrastructure. You register an agent with a declarative spec, a reviewer approves and seats a version, and your worker claims runs, calls Colrows tools with a short-lived run token, and submits a result. Colrows enforces the spec on every tool call, checks that the result's evidence came from the run, and records every run, tool call, and decision. This page is the reference for the spec, the worker protocol, run tokens, the Quorum tools, the output contract, and the admin API.
An organization administrator turns Quorum on in the Quorum settings (PUT /api/quorum/v1/settings with enabled: true). While it is off, nobody holds a Quorum role, no run starts, and workers get 403 QUORUM_DISABLED.
How it works
- Register. An author creates an agent with a unique
agentKey, then writes a draft version: an agent spec that names its semantic scope, its allowed Colrows tools, its identity mode, its limits, its triggers, and its output contract. - Approve. The author submits the draft. Colrows validates the spec and resolves every asset in its scope. A Quorum admin approves or rejects it; when the organization requires evaluation (the default), approval and seating need a passing evaluation at or above the spec's
minimumPassRate. - Seat. A Quorum admin seats an approved version. Only the seated version runs, and seating a new version retires the previous one.
- Run on your infrastructure. A trigger (on demand, a schedule, or an evaluation) queues a run. Your worker claims it, receives the spec, the input, and a run token, and calls the agent tools with that token. The Quorum guard checks each call against the spec.
- Check. The worker submits a result in the ANALYST_ANSWER contract. The Quorum check proves that every piece of evidence is real and was produced by this run. A result that fails the check fails the run.
- Audit. Runs, tool calls, worker events, check findings, and every registry change are recorded and readable through the admin API.
Concepts
| Term | Meaning |
|---|---|
| Agent | A registered agent, identified by agentId (qa_…) and a unique agentKey of 3 to 64 lowercase letters, digits, and hyphens that starts and ends with a letter or digit. It has an owner, a list of invokers (users and groups who may start runs), and a status: ACTIVE, UNSEATED, or RETIRED. |
| Version | A numbered spec of an agent. Its state is DRAFT, IN_REVIEW, APPROVED, REJECTED, SEATED, or RETIRED. Submission records a specDigest (sha256: and the hex SHA-256 of the spec's canonical JSON). |
| Seat / unseat | Seating puts an approved version in service. Unseating is the kill switch: no run of the agent can start until a version is seated again, run tokens stop working at once, queued runs end, and running runs are cancelled at their next heartbeat. Seating the already seated version of an unseated agent puts it back in service. |
| Run | One execution of a seated version (or of any version, for an evaluation), identified by runId (qr_…). Status is QUEUED, RUNNING, CANCEL_REQUESTED, CHECKING, SUCCEEDED, FAILED, CANCELLED, or EXPIRED. |
| Actor | Who the run acts for: USER (the user who started an on-demand run, or the agent owner for a schedule) or AGENT (the agent's own service principal). The run reads data with the actor's access and masking policies. Workers see only the actor's type and display name. |
| Worker | Your software that claims and executes runs. It authenticates with a worker client that an admin creates and that serves a list of agents. |
| Run token | A short-lived bearer token (cr_quorum_rt_…) bound to one run. The worker uses it to call agent tools. It carries only the scopes of the tools the spec allows. |
| Quorum guard | The server-side enforcement of the spec on every agent tool call made with a run token: allowed tools, semantic scope, raw SQL, and budgets. |
| Quorum check | The server-side check of a submitted result: shape, and that its evidence comes from this run. |
Run lifecycle
- A trigger creates a
QUEUEDrun. On-demand runs and schedules respect the agent'smaxConcurrentRunsandmaxRunsPerHour; evaluation runs do not. - A worker claims it: the run becomes
RUNNING, a lease and a run token are issued, and the run's deadline is set tomaxRunSecondsafter the first claim. completemoves the run toCHECKING, runs the Quorum check, and ends itSUCCEEDEDorFAILEDwith codeQUORUM_CHECK_FAILED.failwith a retryable code, or a lease that expires, queues the run again while attempts remain; otherwise the run isFAILED. A run has at most 2 attempts.- Cancelling a queued run ends it at once. Cancelling a running run sets
CANCEL_REQUESTED, revokes its run tokens, and the worker learns at its next heartbeat; the run then becomesCANCELLED. - A run past its deadline becomes
EXPIREDwith failure codeMAX_RUN_SECONDS. A recovery task checks for expired leases and deadlines every minute.
Agent spec
A version's spec is JSON. Drafts are checked for structure; submission also checks that every datasource, metric, and verified query in scope is active in the organization and that each metric and verified query belongs to a datasource in scope.
{
"specVersion": 1,
"agentKey": "revenue-analyst",
"displayName": "Revenue analyst",
"purpose": "Explain week-over-week changes in net revenue.",
"instructions": "Use verified queries. Cite the execution behind every finding.",
"semanticScope": {
"datasourceIds": ["sales-prod"],
"metricIds": ["metric-revenue"],
"verifiedQueryIds": ["vq-123"],
"assetVersionPolicy": "FOLLOW_LATEST"
},
"tools": {
"colrows": ["searchSemanticEntities", "getSemanticEntities", "executeSQL",
"getSQLExecutionStatus", "getSQLExecutionResult",
"getRunContext", "reportSemanticGap"],
"external": []
},
"identity": { "mode": "ON_BEHALF_OF_USER", "fallback": "DENY" },
"limits": { "maxRunSeconds": 300, "maxQueryExecutionsPerRun": 10 },
"triggers": [
{ "kind": "ON_DEMAND" },
{ "kind": "SCHEDULE", "cron": "0 8 * * MON", "zoneId": "Europe/London",
"runAsUserId": "<owner-user-id>", "input": { "question": "Summarize last week's revenue" } }
],
"output": { "contract": "ANALYST_ANSWER", "contractVersion": 1 },
"evaluation": { "suiteIds": ["<suite-id>"], "minimumPassRate": 0.9 }
}
| Field | Rules |
|---|---|
specVersion | Must be 1. |
agentKey | Must equal the agent's key. |
displayName | Required, at most 120 characters. |
purpose | Required, at most 2,000 characters. |
instructions | Optional, at most 20,000 characters. Your worker receives it with the spec. |
semanticScope.datasourceIds | At least one. Every table, column, and dataset of a datasource in scope is in scope. |
semanticScope.metricIds, verifiedQueryIds | Optional. Metrics and verified queries are in scope only when listed by ID. Each list holds at most 200 unique, non-blank IDs (as does datasourceIds). |
semanticScope.assetVersionPolicy | Required: FOLLOW_LATEST, or PINNED to pin the metric and verified-query versions resolved at submission. A pinned agent can run only the pinned version of a verified query. |
tools.colrows | At least one tool, no repeats, from the tool catalog. The run token carries exactly the scopes these tools need. |
tools.external | Must be empty; external tools are not supported. |
identity.mode | Required: ON_BEHALF_OF_USER (the run acts for a user, with that user's access) or SERVICE_PRINCIPAL (the run acts as the agent). |
identity.fallback | Required with ON_BEHALF_OF_USER: DENY or SERVICE_PRINCIPAL. It decides what a schedule without runAsUserId does. |
limits | Optional; each value must be positive and no higher than the organization's maximum. Unset values use the organization default. See limits. |
triggers | 1 to 20 triggers. ON_DEMAND takes no cron or zoneId; only a version with an ON_DEMAND trigger can be started on demand. SCHEDULE needs a five-field cron and an IANA zoneId. A schedule of a version that acts for users needs runAsUserId, which must be the agent owner, unless identity.fallback is SERVICE_PRINCIPAL. input is at most 32 KB. |
output | Required. The only registered contract is ANALYST_ANSWER version 1. |
evaluation | Optional. suiteIds needs at least one suite; minimumPassRate is from 0 to 1. |
Schedule cron fields are minute hour day-of-month month day-of-week. Minutes and hours are numbers or comma lists (no ranges or steps), naming at most 96 times a day. A cron may restrict the day of month or the day of week, not both. Day of week accepts 0 to 7 (0 and 7 are Sunday) and MON to SUN; months accept 1 to 12 and JAN to DEC. Schedules follow the seated version: seating replaces the old version's schedules, and unseating disables them.
Limits
A spec's limits fall back to the organization's default limits, which fall back to the platform defaults. The organization's maximum limits (platform defaults unless set) cap every spec.
| Limit | Platform default | Enforced |
|---|---|---|
maxRunSeconds | 600 | The run's deadline after its first claim. Leases and run tokens never outlast it. |
maxToolCallsPerRun | 200 | Guard: further calls are denied with BUDGET_EXCEEDED. |
maxQueryExecutionsPerRun | 40 | Guard: further executeSQL calls are denied with BUDGET_EXCEEDED. |
maxRowsPerQuery | 5000 | Guard lowers limits.maxRows on executeSQL to this value. |
maxConcurrentRuns | 4 | Runs in progress per agent, and executions in progress per run principal (429 RATE_LIMITED). |
maxRunsPerHour | 120 | Runs created per agent in the last hour. |
maxCostUnitsPerRun | 500 | Each query's estimated cost is charged before it runs; a query that would exceed the remaining budget is denied with BUDGET_EXCEEDED. Unknown cost is not charged. |
Worker protocol
Your worker talks to the worker API at /api/quorum/v1/worker. Every route is POST with a JSON body, returns raw JSON (no envelope) with Cache-Control: no-store, and returns errors as {"code": "…"}.
Authenticate the worker
- A Quorum admin creates a worker client with
POST /api/quorum/v1/worker-clients(see admin API), giving aname, theagentIdsit serves (or["*"]for every agent in the organization), andexpiresAtin epoch milliseconds within one year. The response containsclientIdandclientSecret. Copy the secret straight away: Colrows shows it only once. - The worker exchanges the client ID and secret for an access token, the same way as any OAuth client. A worker client holds only
quorum:workerand can request no other scope.curl -X POST https://cloud.colrows.com/api/oauth/token \ -u "$WORKER_CLIENT_ID:$WORKER_CLIENT_SECRET" \ -d grant_type=client_credentials \ -d scope=quorum:worker - Send the
access_token(cr_oauth_cc_at_…) as a bearer token on every worker call. It lasts 15 minutes; request a new one before it expires. A worker token reaches only the worker API.
Worker clients don't appear in, and can't be revoked from, the general OAuth client settings. Revoke one with POST /api/quorum/v1/worker-clients/{clientId}/revoke.
POST /claims
Claims up to four due runs of agents the worker client serves, in its organization only.
POST /api/quorum/v1/worker/claims
Authorization: Bearer cr_oauth_cc_at_...
{ "workerId": "worker-eu-1", "agentKeys": ["revenue-analyst"], "contractVersion": 1 }
workerId is required, at most 200 characters; use a stable ID per worker process. agentKeys is optional and narrows the claim to those agents among the ones the client serves; keys it doesn't serve are ignored. contractVersion must be 1; any other value, or none, returns 409 CONTRACT_MISMATCH. An empty runs array means there is nothing to do.
{
"runs": [
{
"runId": "qr_8f0c...",
"agent": { "agentKey": "revenue-analyst", "version": 3, "specDigest": "sha256:..." },
"spec": { /* the full agent spec of that version */ },
"input": { "question": "Why did net revenue fall last week?" },
"actor": { "type": "USER", "displayName": "Asha Rao" },
"lease": { "leaseEpoch": 1, "leaseToken": "...", "leaseUntil": "2026-10-31T09:12:00Z" },
"runToken": { "accessToken": "cr_quorum_rt_...", "expiresAt": "2026-10-31T09:25:00Z" },
"endpoints": { "agentRest": "/api/agents/v1", "agentMcp": "/api/mcp/agents" },
"limits": {
"maxRunSeconds": 600, "maxToolCallsPerRun": 200, "maxQueryExecutionsPerRun": 40,
"maxRowsPerQuery": 5000, "maxConcurrentRuns": 4, "maxRunsPerHour": 120,
"maxCostUnitsPerRun": 500
},
"contractVersion": 1
}
]
}
Times are ISO-8601 UTC strings. actor.type is USER or AGENT; the claim never contains the actor's email address, groups, or policies. Keep leaseEpoch and leaseToken: every later call for the run needs them with the same workerId.
POST /runs/{runId}/heartbeat
{ "workerId": "worker-eu-1", "leaseEpoch": 1, "leaseToken": "..." }
{
"leaseUntil": "2026-10-31T09:14:00Z",
"cancelled": false,
"runToken": { "accessToken": "cr_quorum_rt_...", "expiresAt": "2026-10-31T09:29:00Z" }
}
Extends the lease to 120 seconds from now (never shortening it, and never past the run's deadline). When the current run token expires within 5 minutes, the response carries a new runToken; otherwise it carries none, and you keep using the current one. For a run that acts for a user, a token refresh first confirms the user again (still active, still allowed to use the agent, with current directory groups); if that fails, the run is cancelled with code ACTOR_REVOKED. When cancelled is true, the run is cancelled: stop work and make no more calls for it. Heartbeat well inside the 120-second lease, for example every 30 seconds.
POST /runs/{runId}/events
Optional progress events for monitoring. Up to 100 events per call.
{
"workerId": "worker-eu-1", "leaseEpoch": 1, "leaseToken": "...",
"events": [
{ "kind": "MODEL_CALL", "summary": "Planned the revenue breakdown",
"inputTokens": 1830, "outputTokens": 412,
"attributes": { "model": "your-model-name" }, "reportedAt": 1793438400000 }
]
}
kind is required, at most 64 characters. summary is at most 500 characters. attributes is a string-to-string map of at most 20 entries. inputTokens, outputTokens, and reportedAt are optional integers. The response is { "accepted": <count> }. Events are kept for the organization's telemetry retention period.
POST /runs/{runId}/complete
{
"workerId": "worker-eu-1", "leaseEpoch": 1, "leaseToken": "...",
"result": { /* an ANALYST_ANSWER v1 object */ }
}
Submits the result. result is required and must be at most 256 KB as canonical JSON, or the call fails with 413 RESULT_TOO_LARGE. Completion revokes the run's tokens, runs the Quorum check, and returns the outcome:
{
"runId": "qr_8f0c...",
"status": "SUCCEEDED",
"quorumCheck": {
"status": "PASSED",
"findings": [
{ "rule": "FINDING_WITHOUT_EVIDENCE", "severity": "WARNING", "message": "findings[2] has no evidence" }
]
}
}
Completion is idempotent: sending the same result with the same lease credentials again returns the stored outcome. A different result for a run that already has one returns 409 LEASE_OR_STATE_CONFLICT.
POST /runs/{runId}/fail
{
"workerId": "worker-eu-1", "leaseEpoch": 1, "leaseToken": "...",
"code": "MODEL_UNAVAILABLE", "message": "The model endpoint timed out"
}
code must match [A-Z][A-Z0-9_]{0,63}; message is at most 2,000 characters. The codes WORKER_ERROR, MODEL_UNAVAILABLE, NETWORK_ERROR, and TOOL_UNAVAILABLE are retryable: the run is queued again with a backoff that starts at 30 seconds and doubles, while attempts remain and the deadline has not passed. Any other code, or no attempts left, fails the run. The response is { "runId", "status", "quorumCheck": null }, where status is QUEUED or FAILED. The run's tokens are revoked either way.
POST /runs/{runId}/yield
{ "workerId": "worker-eu-1", "leaseEpoch": 1, "leaseToken": "..." }
Returns the run to the queue without using an attempt, for example when the worker is shutting down. The run can be claimed again after about 5 seconds. The response is { "runId", "status": "QUEUED", "quorumCheck": null }, and the run's tokens are revoked.
Worker errors
| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_REQUEST | A required field is missing or a value is out of range. |
| 403 | WORKER_SCOPE_DENIED | The token does not belong to a worker client of this organization. |
| 403 | QUORUM_DISABLED | Quorum is not enabled for the organization (returned by /claims). |
| 404 | RUN_NOT_FOUND | No such run in the worker client's organization. |
| 409 | CONTRACT_MISMATCH | The claim's contractVersion is not 1. |
| 409 | LEASE_OR_STATE_CONFLICT | The worker no longer owns the lease (wrong workerId, leaseEpoch, or leaseToken, or the lease expired), or the run changed state. Stop work on the run. |
| 413 | RESULT_TOO_LARGE | The result is over 256 KB. |
| 503 | WORKER_BACKEND_UNAVAILABLE | A temporary server failure. Retry with backoff. |
A missing or invalid token gets 401, and a token without quorum:worker gets 403, before the route runs; those responses use the standard Colrows error body, not {"code"}.
A worker loop
# Pseudo-code. BASE is your Colrows host, for example https://cloud.colrows.com
token = client_credentials_token(scope="quorum:worker") # refresh before 15 minutes
loop:
runs = POST BASE/api/quorum/v1/worker/claims
{ workerId, contractVersion: 1 }
if runs is empty: sleep a few seconds; continue
for each run (in parallel):
lease = run.lease
runToken = run.runToken.accessToken
start a heartbeat every 30 s:
hb = POST .../runs/{runId}/heartbeat { workerId, leaseEpoch, leaseToken }
if hb.cancelled: stop the run
if hb.runToken: runToken = hb.runToken.accessToken
try:
# Your agent: follow run.spec.instructions, call tools with the run token
# REST: BASE + run.endpoints.agentRest, MCP: BASE + run.endpoints.agentMcp
result = run_agent(run.spec, run.input, runToken)
POST .../runs/{runId}/complete { workerId, leaseEpoch, leaseToken, result }
except model or network failure:
POST .../runs/{runId}/fail { workerId, leaseEpoch, leaseToken,
code: "MODEL_UNAVAILABLE", message }
on 409 LEASE_OR_STATE_CONFLICT: abandon the run
Run tokens and scopes
A run token (cr_quorum_rt_…) is issued at claim and refreshed by heartbeat. It lasts 15 minutes, or until the run's deadline if sooner. It works only while its run is RUNNING and its agent is in service, and Colrows revokes it when the run completes, fails, yields, is cancelled, or the agent is unseated. Only its SHA-256 digest is stored.
- It is accepted only by the agent interface: REST under
/api/agents/v1and MCP atPOST /api/mcp/agents. Every other route refuses it. - It acts as the run's actor: the user the run acts for, with the directory groups resolved for the run, or the agent's service principal. Data access and masking policies apply as for that principal; the token never acts as an administrator.
- Its scopes are exactly the scopes of the tools in
tools.colrows, plusquorum:run. It never carriesdata:sql,semantic:read-write,access-control:read, oraccess-control:read-write. - On agent MCP,
tools/listaddsgetRunContextandreportSemanticGapfor a run token.
Tool catalog
The tools a spec can allow, and the scopes each one puts in the run token. Arguments and results are in the MCP Tool Reference.
| Tool | Scopes |
|---|---|
listDatasources, getJoinGraph, resolveValues | semantic:read |
searchSemanticEntities, getSemanticEntities, searchSqlResolutions, findSqlResolutionMatch | semantic:read, semantic:sql |
validateSQL | sql:execute |
compileSQL, executeSQL, getSQLExecutionStatus, getSQLExecutionResult, cancelSQLExecution | data:query |
askDocuments | documents:read |
getRunContext, reportSemanticGap | quorum:run |
The Quorum guard
The guard runs on every agent tool call made with a run token, on REST and MCP alike. It adds to scope and policy checks; it replaces neither.
- Allowed tools. A tool not in
tools.colrowsis denied, includinggetRunContextandreportSemanticGap. Every call, including a denied one, counts againstmaxToolCallsPerRun. - Datasource scope.
searchSqlResolutions,findSqlResolutionMatch,getJoinGraph,resolveValues, andcompileSQLneed adatasourceIdin scope.searchSemanticEntitieswithoutdatasourceIdsis narrowed to the datasources in scope; with them, each must be in scope. - Queries.
executeSQLruns only verified queries listed inverifiedQueryIds(at the pinned version for aPINNEDscope); raw SQL is denied.validateSQLaccepts a listed verified query, or SQL on a datasource in scope. - Executions.
getSQLExecutionStatus,getSQLExecutionResult, andcancelSQLExecutionwork only on executions this run created. - Result filtering.
listDatasourcesandsearchSemanticEntitiesresults drop out-of-scope items.getSemanticEntitiesmoves out-of-scope entities tounavailable, which gives no reason. Metrics and verified queries count as in scope only when listed by ID. - Budgets. See limits.
A guard denial is 403 ACCESS_DENIED with the reason in quorumDenial on REST, and in _meta["colrows/quorumDenial"] on MCP (next to _meta["colrows/errorCode"]):
{
"type": "about:blank",
"title": "ACCESS_DENIED",
"status": 403,
"detail": "Quorum agents run only verified queries",
"code": "ACCESS_DENIED",
"requestId": "req-123",
"retryable": false,
"quorumDenial": "RAW_SQL_DENIED"
}
quorumDenial | Meaning |
|---|---|
TOOL_NOT_ALLOWED | The spec does not allow the tool. |
OUT_OF_SCOPE | A datasource, verified query, verified-query version, or execution is outside the run's scope. |
RAW_SQL_DENIED | executeSQL was called with raw SQL. |
BUDGET_EXCEEDED | The run used its tool calls, query executions, or cost units. |
Once the run is no longer RUNNING, its run token is refused with 401.
Quorum tools
Two tools exist only for run tokens. Both are available on agent REST and agent MCP with the same input and output, and both must be listed in tools.colrows. Unlike other agent REST routes, their REST response is the result object itself, not wrapped in data and meta. Errors use application/problem+json as described in the MCP Tool Reference.
getRunContext
GET /api/agents/v1/quorum/run
Authorization: Bearer cr_quorum_rt_...
Takes no arguments. Returns the run's agent, scope, allowed tools, limits, and remaining budget:
{
"runId": "qr_8f0c...",
"agent": { "agentKey": "revenue-analyst", "version": 3 },
"actor": { "type": "USER" },
"scope": {
"datasourceIds": ["sales-prod"],
"metricIds": ["metric-revenue"],
"verifiedQueryIds": ["vq-123"],
"assetVersionPolicy": "FOLLOW_LATEST"
},
"tools": ["executeSQL", "getRunContext", "getSQLExecutionResult", "..."],
"limits": { "maxRunSeconds": 300, "maxToolCallsPerRun": 200, "...": "..." },
"remaining": { "toolCalls": 187, "queryExecutions": 8, "costUnits": 455 },
"deadline": "2026-10-31T09:15:00Z",
"requestId": "req-123",
"contractVersion": "1"
}
A remaining value is null when that limit is not set.
reportSemanticGap
POST /api/agents/v1/quorum/gaps
Authorization: Bearer cr_quorum_rt_...
Content-Type: application/json
{ "term": "net retention", "kind": "MISSING_METRIC", "context": "Asked for net retention by cohort" }
Reports a term or concept the semantic layer could not resolve. term is required, 1 to 200 characters. kind is required: UNKNOWN_TERM, AMBIGUOUS_TERM, MISSING_METRIC, or MISSING_RELATIONSHIP. context is optional text of at most 1,000 characters that explains why the term was needed; it is not stored. Other fields are rejected.
{ "gapId": "qg_3b1e...", "occurrences": 4, "inquiryOpened": true, "requestId": "req-123", "contractVersion": "1" }
Colrows keeps one gap per normalized term (lowercase, single spaces) in the organization and counts its occurrences. A reported gap opens a Needs Attention inquiry for curators at once. The guard also records candidate gaps on its own: a searchSemanticEntities call that finds nothing, or a compileSQL result of NEEDS_CLARIFICATION. A candidate gap opens an inquiry only after it occurs in 3 runs.
Output contract: ANALYST_ANSWER v1
{
"contract": "ANALYST_ANSWER",
"contractVersion": 1,
"status": "ANSWERED",
"answer": "Net revenue fell 8% week over week, mostly in EMEA.",
"findings": [
{
"text": "EMEA accounts for 6.1 of the 8 points.",
"evidence": [
{ "kind": "QUERY_EXECUTION", "executionId": "<executionId from executeSQL>" },
{ "kind": "DOCUMENT_CITATION", "documentId": "<documentId>", "versionId": "<versionId>" }
]
}
],
"clarification": null,
"chart": null
}
| Field | Rules |
|---|---|
contract, contractVersion | "ANALYST_ANSWER" and 1. |
status | ANSWERED, NEEDS_CLARIFICATION, or UNABLE_TO_ANSWER. |
answer | Text. Required when status is ANSWERED. |
clarification | Text. Required when status is NEEDS_CLARIFICATION. |
findings | Array of at most 100. Each finding needs text, and may have evidence: an array of at most 50 items. |
evidence[].kind | QUERY_EXECUTION with executionId, or DOCUMENT_CITATION with documentId and versionId. |
chart | An object or null. |
No other top-level fields are allowed.
Quorum check rules
The check runs when the worker completes the run. Any ERROR finding fails the check, and the run ends FAILED with code QUORUM_CHECK_FAILED; the result is kept for review. WARNING findings don't fail the run. If the result fails SCHEMA, the evidence rules are not evaluated.
| Rule | Severity | Fails when |
|---|---|---|
SCHEMA | ERROR | The result does not match the contract above. |
EXECUTION_NOT_FROM_RUN | ERROR | A QUERY_EXECUTION references an execution that no successful executeSQL call of this run created. |
EXECUTION_NOT_SUCCEEDED | ERROR | The referenced execution did not succeed. |
CITATION_NOT_FROM_RUN | ERROR | A DOCUMENT_CITATION names a document version that no askDocuments answer of this run cited. |
UNKNOWN_EVIDENCE | ERROR | An evidence item has an unknown kind. |
ANSWER_WITHOUT_EVIDENCE | ERROR | An ANSWERED result has no finding with evidence. |
FINDING_WITHOUT_EVIDENCE | WARNING | A finding has no evidence. |
COMPILED_QUERY | WARNING | The run called compileSQL and the evidence uses an execution that is not a verified query. |
The check proves that the evidence is real and was produced by this run. It cannot prove that the answer text follows from the evidence; evaluation suites measure that.
Admin API
The admin API under /api/quorum/v1 is for the Colrows UI and administration. It needs a signed-in user's session token; OAuth client, MCP, worker, and run tokens are refused. These routes are not agent tools and are not available on agent MCP.
Responses use the standard Colrows REST envelope, with the data in payload. Draft edits, review, and access changes accept an optional expectedRevision in the body (submission takes it as a query parameter) and fail with 409 when the record changed.
Who can do what
| Quorum role | Grants |
|---|---|
VIEWER | Agents, versions, run metadata, tool-call telemetry, worker events, evaluation results, and gaps. Not run results. |
AUTHOR | VIEWER, plus create agents, edit and submit drafts, manage evaluation suites, and start evaluations. |
ADMIN | AUTHOR, plus review, seat, unseat, worker clients, role assignments, and every run result. |
An organization administrator holds every Quorum role. Other users hold a role when their user ID, or an active user group they belong to, is assigned that role or a higher one. Roles are resolved on each request, and nobody holds one while Quorum is disabled. Separately, an agent's owner and invokers decide who can start its runs, and only the user who started a run, the user it acts for, the agent owner, or a Quorum admin can read its result.
Routes
| Route | Purpose | Access |
|---|---|---|
GET, PUT /settings | Quorum settings other than roles: enabled, requireEvaluation (default true), captureInputs, unseatOnBrokenScope, telemetryRetentionDays (7 to 400, default 90), defaultLimits, maximumLimits | Organization administrator, also while Quorum is disabled |
GET, PUT /roles | Role assignments: each with a role, userIds, and groupIds | ADMIN |
GET /audit-events?agentId= | Audit events of one agent, or of the organization without agentId | VIEWER / ADMIN |
GET /agents, POST /agents | List agents; create one with agentKey, displayName, optional ownerUserId (defaults to you), and invokers | VIEWER / AUTHOR |
GET /agents/{agentId} | Agent detail with its versions and schedules | VIEWER |
PUT /agents/{agentId}/access | Change the owner and invokers | Owner or ADMIN |
POST /agents/{agentId}/client/rotate | Replace the agent's dedicated client, which ends its run tokens | ADMIN |
POST /agents/{agentId}/versions | Create a draft from a full spec, or fromVersion to copy an earlier spec | AUTHOR |
GET, PUT /agents/{agentId}/versions/{version} | Read a version; update a draft | VIEWER / AUTHOR |
POST /agents/{agentId}/versions/{version}/submit | Validate and submit a draft for review | AUTHOR |
POST /agents/{agentId}/versions/{version}/review | decision APPROVE or REJECT, optional note (at most 2,000 characters) | ADMIN |
POST /agents/{agentId}/versions/{version}/seat | Seat an approved version | ADMIN |
POST /agents/{agentId}/unseat | Kill switch, with a required reason (at most 2,000 characters) | ADMIN |
POST /agents/{agentId}/runs | Start an on-demand run with { "input": {…} } (at most 32 KB). An optional Idempotency-Key header (1 to 200 characters) returns the same run for the same input; a different input with the same key gets 409. Too many runs gets 429. | Owner, invoker, or ADMIN |
GET /runs?agentId=, GET /runs/{runId} | Run metadata, newest first, without results | VIEWER |
GET /runs/{runId}/result | The result and its Quorum check | Starter, actor, owner, or ADMIN |
POST /runs/{runId}/cancel | Cancel a run | Starter, owner, or ADMIN |
GET /runs/{runId}/tool-calls | Tool-call telemetry of a run | VIEWER |
GET /runs/{runId}/events | Events the worker reported | VIEWER |
GET /gaps | Semantic gaps, most recently seen first | VIEWER |
GET, POST /evaluation-suites; GET, PUT /evaluation-suites/{suiteId} | Evaluation suites | VIEWER to read, AUTHOR to write |
POST /agents/{agentId}/versions/{version}/evaluations | Evaluate a version against the suites its spec names | AUTHOR |
GET /agents/{agentId}/evaluations, GET /evaluations/{evaluationRunId} | Evaluation runs and their scores | VIEWER |
GET, POST /worker-clients | List worker clients; create one with name, agentIds (agent IDs, or only "*"), and expiresAt. The secret is returned only in the create response. | ADMIN |
POST /worker-clients/{clientId}/revoke | Revoke a worker client | ADMIN |