Agent run-result API
The orchestrator exposes a machine-first read surface for run state, designed so an automation agent can understand a run as structure — a typed job DAG with per-step exit codes, durations, statuses, and a derived failure classification — instead of scraping stderr. Every field that originates from outside KiCI is tagged so a consumer can keep user-controlled content out of an agent’s instruction channel.
These endpoints live on the orchestrator admin HTTP API and reuse its authentication: a
Bearer token validated against the orchestrator, the run.read RBAC permission, and
routing-key confinement (a routing-key-scoped token can only read runs under its own
routing key). They are read-only.
Provenance tagging
Section titled “Provenance tagging”Each field is either trusted or untrusted:
-
Trusted (plain value): values KiCI itself generates or measures — run / job / step ids, enum statuses, exit codes, durations, timestamps, commit hashes, the derived failure category, the step type, the check outcome.
-
Untrusted (enveloped): values sourced from the user’s repository, the contributor, or a process’s output — workflow / repository / job / step names, the git ref, the contributor username, error messages, job output values, and every log line. Each is wrapped:
{ "untrusted": true, "value": "deploy to production" }
The envelope self-describes, so the tag survives reshaping. A consumer that fences
untrusted content refuses to render any { "untrusted": true } value into an
instruction channel, while still showing it as data.
Endpoints
Section titled “Endpoints”Structured run result
Section titled “Structured run result”GET /api/v1/admin/runs/:runId/structuredReturns the run header plus the full job DAG. Trusted fields are plain; untrusted fields are enveloped. Shape (abridged):
{ "runId": "run_123", "workflowName": { "untrusted": true, "value": "ci" }, "status": "failed", "provider": "github", "repoIdentifier": { "untrusted": true, "value": "owner/repo" }, "ref": { "untrusted": true, "value": "refs/heads/main" }, "sha": "abc123", "baseSha": null, "startedAt": "2026-06-27T00:00:00.000Z", "completedAt": "2026-06-27T00:01:00.000Z", "durationMs": 60000, "trustTier": "trusted", "contributorUsername": { "untrusted": true, "value": "alice" }, "failureCategory": "step_failed", "failureReason": { "untrusted": true, "value": "tests failed" }, "triggeredBy": null, "jobs": [ { "jobId": "job_1", "jobName": { "untrusted": true, "value": "build" }, "status": "failed", "durationMs": 42000, "agentId": "agent-7", "errorMessage": { "untrusted": true, "value": "step 'test' failed" }, "initFailure": null, "needs": [{ "ref": { "untrusted": true, "value": "lint" }, "runOn": ["success"] }], "outputs": { "build.url": { "untrusted": true, "value": "https://..." } }, "secretOutputKeys": ["DEPLOY_TOKEN"], "steps": [ { "stepIndex": 0, "stepName": { "untrusted": true, "value": "test" }, "status": "failed", "exitCode": 1, "durationMs": 12000, "stepType": "step", "checkOutcome": null, "secretsAccessed": ["DEPLOY_TOKEN"], "phase": "steps" } ] } ]}Each step carries phase (setup, init or steps). An init command also carries
initIndex, its position in the job’s init list. Its stepIndex is a large internal number.
baseSha is best-effort from the run’s provider context; it is null when the base commit
is not available. triggeredBy is the identity that triggered a re-run, and null for
webhook-triggered runs. triggeredByAgentLabel names the agent credential label when a run was
triggered through an agent credential. It is absent or null for every other run.
Step logs
Section titled “Step logs”GET /api/v1/admin/runs/:runId/jobs/:jobId/steps/:stepIndex/logs?cursor=&limit=Returns a page of a step’s log lines, every line enveloped untrusted (log output is
user/process-controlled). Pagination is line-based: cursor is a line offset, limit
defaults to 500 and is capped at 2000. nextCursor is the offset to fetch the next page,
or null when the last line has been returned. recorded tells an empty page apart: false
when the step never wrote a log, true when a log is stored, even one with no lines. Step
-1 is the job’s setup log: the clone, the dependency install and the workflow module load.
{ "runId": "run_123", "jobId": "job_1", "stepIndex": 0, "totalLines": 1200, "lines": [{ "untrusted": true, "value": "Running tests..." }], "nextCursor": "500", "recorded": true}A global workflow’s pre-run evaluation round is readable here too. Such a round decides
whether a run happens at all, so when it admits its candidates it records no run of its
own — its runId is resolved from the dispatch queue instead. The token’s routing-key
scope is enforced exactly the same way, and a runId that belongs to neither an
execution nor the dispatch queue still returns 404. A runId that is not a full UUID returns 400, and the error names it.
Derived failure category
Section titled “Derived failure category”failureCategory is a trusted, coarse classification derived from data KiCI already
records (no heuristics). It is null for a run that did not fail, and otherwise one of:
| Category | When |
|---|---|
init_failure | The run/job recorded an init-phase failure (e.g. secret resolution). |
infra | The init failure was an infrastructure cause (no agent available). |
timed_out | A job hit a configured wall-clock timeout. |
cancelled | The run was cancelled. |
step_failed | A step exited non-zero. |
unknown | The run failed with no clearer signal. |
Secret safety
Section titled “Secret safety”Secret output values are never returned by these endpoints. A job’s non-secret outputs
appear as enveloped values under outputs; secret outputs appear only as key names under
secretOutputKeys, and secretsAccessed lists key names a step touched. Recovering a
secret value is a separate, audited break-glass path (runs secret-outputs --reveal).
See also
Section titled “See also”- kici-admin CLI —
kici-admin runs structured <runId> [--json]wraps the structured endpoint.