Skip to content

Factory API ​

These five observational routes return versioned factory work and repository observations. Availability depends on the installed version. Two more routes read the audit ledger and the delivery metrics, and one protected route records review acceptance declarations; see Audit events and delivery metrics. One protected route checks repository setup prerequisites without writing; see Setup preflight.

Route classification ​

The classes below describe behavior and access, not the HTTP method. "Operator token" means the X-Deck-Operator-Token header. "Mail session" means an authenticated X-Deck-Session-Token.

A route with Operator token access accepts only the operator token; a session token never opens it. A route that accepts both principals authenticates a non-empty session header first, also when both headers are sent. With no session header, or an empty one, it requires the operator token.

ClassRouteAccessBehavior
Safe observationGET /api/v1/factory/overview, /api/v1/factory/work-items, /api/v1/factory/work-items/{item_id}, /api/v1/factory/repositories, /api/v1/factory/repositories/{scope_id}NoneReads local records and a bounded scheduler observation
Safe observationGET /api/v1/factory/metricsNoneReads ledger aggregates
Protected observationGET /api/v1/factory/audit-eventsOperator tokenReads ledger history
Protected observationPOST /api/v1/factory/setup-preflightOperator tokenChecks the checkout, GitHub access, labels, base branch and selected authentication presence; creates or changes no records
Protected observationGET /api/v1/agent-teams/github-work-items/{item_id}/scope-revisionsOperator token, or a Mail session of the same teamReads revision history; private commands only for the operator or the matched current owner
Protected declarationPOST /api/v1/factory/review-acceptancesOperator tokenRecords one audit declaration; never changes work state, approvals, merges, retries, leases or counters
Protected mutationPOST /api/v1/agent-teams/github-work-items/{work_item_id}/retryOperator token, or the Mail session of the current Leader under the conditions belowResets the item for retry after current state and eligibility checks

A Mail session for retry qualifies only when all of these are true:

  • The backend enforces Agent Mail capability tokens (mail_capability_tokens_required=true).
  • The session comes from MCP and its mailbox is connected.
  • The session belongs to the member of the team's current enabled Leader slot.

Otherwise the route returns 403 current_leader_required. Other team and recovery routes are in the Agent Teams API.

All paths below begin with /api/v1/factory. These GET routes read local factory records and a bounded scheduler observation. They do not dispatch, claim a lease, send or acknowledge Mail, launch a provider, change the active project, or fetch GitHub issues. They have no operator/session authentication dependency in this source. Protected recovery reads and mutations have their own authentication and state checks.

Method and pathResponse
GET /overviewSelected work counts, configured automation and observed intake
GET /work-itemsPaginated safe work projections and complete filtered counts
GET /work-items/{item_id}One safe work projection
GET /repositoriesPaginated watched scopes with intake, poll and overlap observations
GET /repositories/{scope_id}One watched scope observation

Filters and pagination ​

Overview and both lists accept optional team_id, scope_id and provider. IDs must be positive integers no larger than 2^63−1. A scope selected with a team must belong to that team. provider must be a registered harness ID: claude-code, codex-cli, copilot-cli, opencode-cli or pi-cli.

For work counts/lists, provider matches the assigned owner's currently configured harness. An unassigned or invalid owner is not replaced with a default provider and does not match a provider filter. For repository selection, provider matches a configured slot in that scope's team roster; scopes have no assigned owner. These filters therefore describe different sets. Configured harness and observed session provider are separate fields.

The work list also accepts category: all (default), queued, active, review, attention, finished or unknown. Both lists accept limit from 1 to 100 (default 50) and optional cursor. Details accept their path ID, without list filters or pagination.

List records sort by updated timestamp descending, then ID descending. Treat the cursor as opaque; pass next_cursor back with the same filters and list type. A changed filter, malformed cursor or incompatible cursor requires a fresh request from the start. A response's count/list/authority reads share a database snapshot; subsequent pages can observe later changes. updated_at describes a persisted record update, not an execution heartbeat.

text
GET /api/v1/factory/work-items?team_id=1&category=attention&limit=50
GET /api/v1/factory/repositories?provider=codex-cli&limit=50

The IDs in examples are synthetic. Repeated GitHub names and issues in different scopes remain independent records.

Response envelopes ​

Successful responses include schema_version: 1 and UTC ISO-8601 generated_at. Optional values are explicit null; arrays are empty arrays when no records match.

RouteAdditional top-level fields
Overviewfilters, counts, automation
Work listfilters, total, has_more, next_cursor, counts, items
Work detailwork_item
Repository listfilters, total, has_more, next_cursor, repositories
Repository detailrepository

total and category counts describe the full selected dataset before pagination, including the selected category. Repository total counts scopes, not distinct GitHub names. next_cursor is null exactly when has_more is false.

Work projections contain an allowlisted item, team/repository context, category, nullable owner/approver/waiting/workspace, separate owner and approver session associations, policy, action observations and typed link hints. Basic details do not expand the private legacy payload. Dispatch nonces/head identity, host workspace paths, credentials/hashes, private commands and freeform recovery summaries are omitted. Last verified SHA is nullable and belongs only to the displayed PR; it is null for diagnostic execution. Treat titles and raw statuses as display text.

Status and action meaning ​

Raw tracking statusCategory
pendingqueued
dispatched, verifyingactive
awaiting_human_review, ready_for_reviewreview
escalated, failedattention
merged, completedfinished
Any unrecognized stateunknown, with the raw state retained

Finished counts describe tracking state, not measured delivery success or human acceptance. Operator-requested escalation remains attention and does not prove that the operating-system process stopped or that a terminal delivery outcome occurred. Authoritative manual and issue-update retry rules remain intact.

An action has name, state, block_code, reason and required_actor. Retry uses the existing retry predicate; it can be blocked by active continuation authority, pending approval or a preserved PR. The other five remedies report unknown eligibility in this API. A configured continuation flag is not permission to resume a prepared attempt. No action observation authorizes its viewer: protected mutations check the current principal and state again. Retry accepts the configured operator or an eligible authenticated current-Leader MCP session. Protected revision history also checks its caller and limits private commands by principal. Agent decisions and human PR review remain separate.

Session state can be bound, offline, ambiguous or unknown. Only a verified association provides a concrete team/slot/member/MCP-session target. Nullable Mail or offline launch hints identify context, not an instruction to send, claim or launch.

Repository intake and overlap ​

Each repository projection is one scope. Team automation and scope enablement determine configured enablement. Effective intake is eligible, blocked or unknown based on that configuration plus normal/recovery-only/unknown runtime mode, running/stopped/unknown scheduler state and whether its job is scheduled. Runtime has its own observation timestamp, distinct from response generation. Configuration alone does not prove intake is running.

Polling freshness is fresh, stale, never_polled, suspended or unknown. Stale means an eligible scope's last poll is older than twice the configured interval. Blocked or paused intake suspends freshness; unknown runtime remains unknown. Overview stale/never-polled counts include eligible intake only.

Overlap warns when enabled teams/scopes share a normalized GitHub repository and dispatch label. It checks local scopes outside the selected filters and returns safe other scope IDs. A warning neither merges work records nor arbitrates dispatch ownership.

Errors ​

New read errors use detail: {code, message}.

HTTP statusCodeMeaning
422invalid_filterInvalid IDs, category, provider, limit or incompatible filters
422invalid_cursorMalformed, wrong-version, wrong-list or filter-mismatched cursor
404resource_not_foundSelected team, scope or work item is absent
500projection_failedDatabase/projection observations could not be loaded
json
{"detail":{"code":"invalid_cursor","message":"Refresh from start with the selected filters."}}

Preserve both code and message. A read failure is an error, not a zero count. Existing protected routes retain their own string or structured error details and their 401/403/409 semantics; this contract does not rewrite them.

Synthetic response and source ​

The following overview comes from checked-in disposable fixtures, not a live factory capture:

json
{
  "automation": {
    "configured_scopes": 3,
    "enabled_scopes": 2,
    "intake_blocked_scopes": 0,
    "intake_eligible_scopes": 2,
    "intake_unknown_scopes": 0,
    "never_polled_scopes": 1,
    "paused_scopes": 1,
    "runtime": {
      "mode": "normal",
      "observed_at": "2026-09-30T11:59:00Z",
      "reason_code": null,
      "scheduler_state": "running"
    },
    "stale_scopes": 0
  },
  "counts": {
    "active": 27,
    "attention": 26,
    "finished": 26,
    "queued": 14,
    "review": 26,
    "total": 132,
    "unknown": 13
  },
  "filters": {
    "provider": null,
    "scope_id": null,
    "team_id": null
  },
  "generated_at": "2026-09-30T12:00:00Z",
  "schema_version": 1
}

Complete nested schemas, nullable fields and bounded reason mappings are in backend/app/models/factory_schemas.py and backend/tests/factory/fixtures/v1/. The checked-in fixtures are synthetic examples of this versioned contract.

Audit events and delivery metrics ​

These two GET routes read the observation ledger. They never fetch GitHub facts and never change a record. The audit and metrics guide explains the numbers.

Method and pathAuthenticationParameters
GET /audit-eventsOperator token header X-Deck-Operator-Tokenpage (default 1), page_size (1–100, default 25), event_kind, team_context_key, scope_context_key, item_context_key, team_id, scope_id, item_id
GET /metricsNonewindow_start, window_end (required), filter_scope (all or scoped), team_context_key, scope_context_key

Audit events accept pagination and context filters, without a time window. Metrics accept a time window and context filters, without pagination. Agent session tokens do not authorize audit reads. A missing backend operator token returns 503; a missing or invalid header returns 401.

An audit page has items, total, page, page_size, the applied keys and snapshot_labels (the observed snapshot keys in the page). Each item has its kind, source, record kind, fact source and fact time, actor kind and reference, live links and context keys, a typed safe context_snapshot, a sanitized reason, allowlisted before and after values, and the action, delivery and completion outcomes. Member and session IDs, operation identities and replay keys are not returned. live_links_available is false when every live link was removed by deletion.

A current-ID filter (team_id, scope_id, item_id) resolves the active context key of the current resource lifetime, without writing. An ID with no recorded lifetime returns no events. A context key also addresses its own history after deletion; a reused numeric ID has a new key.

A metrics window has window_start, window_end, filter_scope, counting_unit_note, instrumentation_start, available_interval_start, available_interval_end, missing_intervals and metrics. instrumentation_start is the installed coverage marker, not the first event. A scoped request without a key returns no metrics. Each sample has name, counting_unit, value (null when unknown), sample_count, unknown_count, excluded_count, unknown_reasons, source and coverage. Ledger samples say full, partial or unavailable coverage. Outcome samples count one current result per tracked attempt. Present-state samples name their population. A key without a current resource gives unknown present-state values.

Review acceptance declarations ​

POST /review-acceptances records one review acceptance from a trusted source. It needs the operator token header X-Deck-Operator-Token. It is observational: it never changes work state, approvals, merges, retries, leases or counters.

FieldRule
declaration_id8–64 characters (A-Z a-z 0-9 . _ : -). The idempotency key.
work_item_idAn existing work item.
attemptThe attempt key item:{id}:launch:{launch}:revision:{revision} of that item.
artifactowner/repo/pull/N in the item's repository.
versionThe 40-character lowercase commit SHA that was reviewed.
reviewerThe attributed reviewer. operator, shared-operator-credential and member: references are refused.
reviewer_kindhuman, as declared by the source.
independentAs declared by the source.
decisionaccepted or rejected.
occurred_atThe review time, with a timezone, not in the future.
source_kind, source_refgithub_review, signed_record or operator_attested, and a safe source reference.

Responses:

  • 201 with event_id, operation_id, counted and delivery_established when the declaration is recorded.
  • 200 with the stored result for an exact replay.
  • 409 with refusal when the declaration does not bind: attempt_unrelated, artifact_unrelated, version_changed, version_unrelated, work_item_not_found or replay_conflict. The refusal is recorded as a rejected event.
  • 422 for a field that breaks the contract, and 401 or 503 for the operator token, with no write.

An accepted, independent, human declaration for a design item also records delivery of that exact version. The recording credential is stored as the recording actor, separate from the reviewer.

Setup preflight ​

POST /api/v1/factory/setup-preflight checks repository setup prerequisites for guided configuration. It needs the operator token header X-Deck-Operator-Token. It is an observation: it creates or changes no records, creates no labels and starts no issue intake. It reads the local checkout and GitHub.

Request fieldRule
repo_owner, repo_name1–100 characters (A-Z a-z 0-9 . _ -)
repo_pathThe primary checkout path, 1–2048 characters
dispatch_label, design_label1–100 characters
dispatch_auth_modetoken or github_app
base_refDefault origin/HEAD, 1–255 characters

The response has:

  • status: ready, blocked or unknown.
  • observed_at and checked_at.
  • checks: one entry for each check, with status (ready, blocked or unknown), a short code and a remedy.
  • configuration_presence: allowlisted setting names mapped to true or false only. It never contains values, key-file paths, hashes or raw environment output.
  • host_guidance: static host-procedure steps.

A ready result describes this observation only. It does not verify model access or credential validity for later work. Activation in the guided flow needs a fresh ready result. A missing or invalid operator token is refused before any check runs.

Released under the MIT License.