Skip to content

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.

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.

GET /api/v1/admin/runs/:runId/structured

Returns 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.

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.

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:

CategoryWhen
init_failureThe run/job recorded an init-phase failure (e.g. secret resolution).
infraThe init failure was an infrastructure cause (no agent available).
timed_outA job hit a configured wall-clock timeout.
cancelledThe run was cancelled.
step_failedA step exited non-zero.
unknownThe run failed with no clearer signal.

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).

  • kici-admin CLI — kici-admin runs structured <runId> [--json] wraps the structured endpoint.