Approval holds
KiCI gates execution on human approval at step, job, and workflow granularity. Every gate, whatever its source, produces the same artifact — a held element that pauses execution until its approval requirement is satisfied, rejected, or expired. This page describes the unified model and the step-level round-trip that lets a hold land mid-job.
For authoring gates see Approval gates (user guide); for operating them see Approval gates (operator guide).
One hold, two triggers
Section titled “One hold, two triggers”Two independent sources can hold an element. They differ only in what triggers the hold and where the approver requirement comes from — both funnel into the same held-element mechanism.
| Source | Trigger | Requirement source | Granularity |
|---|---|---|---|
| Mandatory | element targets a protected environment | environment policy (required reviewers) | job |
| Explicit | author wrote requireApproval in the SDK | the clauses in code, resolved against operator teams | step / job / workflow |
The explicit requireApproval declaration is compiled into the lock file’s approval block (at the matching step, job, or workflow node). The mandatory requirement is resolved at dispatch time from the environment’s reviewer policy. Both normalize to one shape before the gate evaluates them:
ApprovalRequirement = { clauses: ApproverClause[] // AND — all must be satisfied expiresAt: timestamp reason: string}ApproverClause = { team: string } | { user: string }When both a mandatory environment hold and an explicit hold apply to the same job, their clauses are combined into one requirement (AND), so both sources must be satisfied.
Clause evaluation
Section titled “Clause evaluation”A requirement is satisfied when all of its clauses are satisfied:
{ team: T }is satisfied once any member of teamTapproves.{ user: U }is satisfied onceUapproves.- An empty clause list (
requireApproval: true) is satisfied by a single approval from any approval-capable member.
Clauses are a flat AND list; one qualifying approver may satisfy several clauses at once (an approver who is both in team leads and is user cto satisfies both clauses with one decision). Any single rejection rejects the whole element; an expired hold is treated as a rejection.
The orchestrator has no identity store of its own. Team membership and identity links arrive over the control-plane trust-policy push and are cached in memory; clause matching and approver eligibility read only that cached snapshot, never anything carried on the approval request itself. This is the same trust boundary the rest of the CI-security path uses.
Each individual decision is recorded — the approver, the decision, and which clauses it satisfied — so multi-clause progress and per-approver attribution are first-class in the dashboard queue and on the run detail page. Eligibility is enforced at approve time: an actor must be eligible for at least one unsatisfied clause, and self-approval is rejected when the org disables it.
Hold lifecycle and resume
Section titled “Hold lifecycle and resume”A held element reuses the execution state machine: the held state and the HOLD / APPROVE / REJECT / EXPIRE events. There are no approval-specific states — workflow- and step-level holds use the same held state as the existing job-level hold.
On full satisfaction the held element is resumed, through one path shared by the dashboard and CLI approve flows:
- Job or workflow scope — the released element is re-dispatched (enqueued for dispatch). A workflow-level hold gates the run’s first dispatch; releasing it lets the run’s jobs proceed.
- Step scope — the orchestrator signals the waiting agent (see the round-trip) rather than enqueuing anything.
A rejection or an expiry instead fails the element via the REJECT / EXPIRE transition, which fails the run. The stale run detector sweeps overdue holds and drives the expiry side.
Step-level round-trip
Section titled “Step-level round-trip”A step-level gate must pause a job mid-execution, after earlier steps have run, with the workspace and prior-step state intact. The agent runs a job as one unit, so this requires a round-trip between the agent and its orchestrator over two protocol messages on the orchestrator ↔ agent channel.
mermaid render failed:
Parse error on line 9:...locks step loop;<br/>keeps heartbeating-----------------------^Expecting 'NEWLINE', ',', '()', 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'SOLID_ARROW_TOP', 'SOLID_ARROW_BOTTOM', 'STICK_ARROW_TOP', 'STICK_ARROW_BOTTOM', 'SOLID_ARROW_TOP_DOTTED', 'SOLID_ARROW_BOTTOM_DOTTED', 'STICK_ARROW_TOP_DOTTED', 'STICK_ARROW_BOTTOM_DOTTED', 'SOLID_ARROW_TOP_REVERSE', 'SOLID_ARROW_BOTTOM_REVERSE', 'STICK_ARROW_TOP_REVERSE', 'STICK_ARROW_BOTTOM_REVERSE', 'SOLID_ARROW_TOP_REVERSE_DOTTED', 'SOLID_ARROW_BOTTOM_REVERSE_DOTTED', 'STICK_ARROW_TOP_REVERSE_DOTTED', 'STICK_ARROW_BOTTOM_REVERSE_DOTTED', 'BIDIRECTIONAL_SOLID_ARROW', 'DOTTED_ARROW', 'BIDIRECTIONAL_DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', 'SOLID_POINT', 'DOTTED_POINT', 'TXT', got 'INVALID'
sequenceDiagram participant Agent participant Orchestrator participant Approver
Note over Agent: runs earlier steps Agent->>Orchestrator: step.approval-request<br/>(runId, jobId, stepIndex, requirement) Note over Orchestrator: create step-scoped held element Note over Agent: blocks step loop;<br/>keeps heartbeating Approver->>Orchestrator: approve (dashboard / kici approve) Note over Orchestrator: all clauses satisfied → release Orchestrator->>Agent: step.approval-resolved<br/>(outcome: approved) Note over Agent: runs the held step,<br/>then continues the jobstep.approval-request(agent → orchestrator) carriesrunId,jobId,stepIndex,stepName, and the normalizedrequirement. The orchestrator creates a step-scoped held element for it.- The agent blocks its step loop and
awaits resolution, keeping the sandbox and workspace live. Heartbeats continue throughout so the agent is not reaped while waiting. step.approval-resolved(orchestrator → agent) carriesrequestId(correlating to the request),runId,jobId,stepIndex, and anoutcomeofapproved,rejected, orexpired. Onapprovedthe agent runs the held step against its intact workspace and continues the job. Onrejectedorexpiredit fails the job with a clear reason.
These two messages are ordinary protocol messages; they do not affect the heartbeat and log-chunk fast paths.
Because a step-level hold keeps an agent and workspace occupied for the whole wait, it is bounded by the hold’s expiry. Operators size this with approval_expiry_seconds (or a per-gate timeout); see the agent-occupancy note.
See also
Section titled “See also”- Approval gates (user guide) — authoring
requireApproval. - Approval gates (operator guide) — teams, the queue, expiry, self-approval.
- Execution state machine — the
heldstate and its transitions. - Orchestrator ↔ Agent messages — the protocol channel the step round-trip rides.