docs/guides/execution-policy.md
Paperclip's execution policy system ensures tasks are completed with the right level of oversight. Instead of relying on agents to remember to hand off work for review, the runtime enforces review and approval stages automatically.
An execution policy is an optional structured object on any issue that defines what must happen after the executor finishes their work. It supports three layers of enforcement:
| Layer | Purpose | Scope |
|---|---|---|
| Comment required | Every agent run must post a comment back to the issue | Runtime invariant (always on) |
| Review stage | A reviewer checks quality/correctness and can request changes | Per-issue, optional |
| Approval stage | A manager/stakeholder gives final sign-off | Per-issue, optional |
These layers compose. An issue can have review only, approval only, both in sequence, or neither (just the comment-required backstop).
executionPolicy)interface IssueExecutionPolicy {
mode: "normal" | "auto";
commentRequired: boolean; // always true, enforced by runtime
stages: IssueExecutionStage[]; // ordered list of review/approval stages
}
interface IssueExecutionStage {
id: string; // auto-generated UUID
type: "review" | "approval"; // stage kind
approvalsNeeded: 1; // multi-approval is not supported yet
participants: IssueExecutionStageParticipant[];
}
interface IssueExecutionStageParticipant {
id: string;
type: "agent" | "user";
agentId?: string | null; // set when type is "agent"
userId?: string | null; // set when type is "user"
}
Participants can be either agents or board users. Each stage can have multiple participants; the runtime selects the first eligible participant, preferring any explicitly requested assignee while excluding the original executor.
executionState)Tracks where the issue currently sits in its policy workflow:
interface IssueExecutionState {
status: "idle" | "pending" | "changes_requested" | "completed";
currentStageId: string | null;
currentStageIndex: number | null;
currentStageType: "review" | "approval" | null;
currentParticipant: IssueExecutionStagePrincipal | null;
returnAssignee: IssueExecutionStagePrincipal | null;
completedStageIds: string[];
lastDecisionId: string | null;
lastDecisionOutcome: "approved" | "changes_requested" | null;
}
issue_execution_decisions)An audit trail of every review/approval action:
interface IssueExecutionDecision {
id: string;
companyId: string;
issueId: string;
stageId: string;
stageType: "review" | "approval";
actorAgentId: string | null;
actorUserId: string | null;
outcome: "approved" | "changes_requested";
body: string; // required comment explaining the decision
createdByRunId: string | null;
createdAt: Date;
}
┌──────────┐ executor ┌───────────┐ reviewer ┌───────────┐ approver ┌──────┐
│ todo │───completes───▶│ in_review │───approves───▶│ in_review │───approves───▶│ done │
│ (Coder) │ work │ (QA) │ │ (CTO) │ │ │
└──────────┘ └───────────┘ └───────────┘ └──────┘
executionPolicy specifying a review stage (e.g., QA) and an approval stage (e.g., CTO).in_progress status.done — the runtime intercepts this:
in_review (not done)executionState enters pending on the review stagedone with a comment:
{ outcome: "approved" }in_review, reassigned to the approverexecutionState advances to the approval stagedone with a comment:
{ outcome: "approved" }executionState.status becomes completeddone status┌───────────┐ reviewer requests ┌─────────────┐ executor ┌───────────┐
│ in_review │───changes────────────▶│ in_progress │───resubmits──▶│ in_review │
│ (QA) │ │ (Coder) │ │ (QA) │
└───────────┘ └──────────────┘ └───────────┘
done (typically in_progress), with a comment explaining what needs to change.in_progressreturnAssignee)executionState.status to changes_requesteddone again.Review only (no approval stage):
{
"stages": [
{ "type": "review", "participants": [{ "type": "agent", "agentId": "qa-agent-id" }] }
]
}
Executor finishes → reviewer approves → done.
Approval only (no review stage):
{
"stages": [
{ "type": "approval", "participants": [{ "type": "user", "userId": "manager-user-id" }] }
]
}
Executor finishes → approver signs off → done.
Multiple reviewers/approvers: Each stage supports multiple participants. The runtime selects one to act, excluding the original executor to prevent self-review.
Independent of review stages, every issue-bound agent run must leave a comment. This is enforced at the runtime level:
issueCommentStatus is set to retry_queued, and the agent is woken once more with reason missing_issue_comment.issueCommentStatus is set to retry_exhausted. No further retries. The failure is recorded.issueCommentStatus is set to satisfied and linked to the comment ID.This prevents silent completions where an agent finishes work but leaves no trace of what happened.
| Field | Description |
|---|---|
issueCommentStatus | satisfied, retry_queued, or retry_exhausted |
issueCommentSatisfiedByCommentId | Links to the comment that fulfilled the requirement |
issueCommentRetryQueuedAt | Timestamp when the retry wake was scheduled |
currentParticipant in execution state) can advance or reject the current stage.422 Unprocessable Entity error.POST /api/companies/{companyId}/issues
{
"title": "Implement feature X",
"assigneeAgentId": "coder-agent-id",
"executionPolicy": {
"mode": "normal",
"commentRequired": true,
"stages": [
{
"type": "review",
"participants": [
{ "type": "agent", "agentId": "qa-agent-id" }
]
},
{
"type": "approval",
"participants": [
{ "type": "user", "userId": "cto-user-id" }
]
}
]
}
}
Stage IDs and participant IDs are auto-generated if omitted. Duplicate participants within a stage are automatically deduplicated. Stages with no valid participants are removed. If no valid stages remain, the policy is set to null.
PATCH /api/issues/{issueId}
{
"executionPolicy": { ... }
}
If the policy is removed (null) while a review is in progress, the execution state is cleared and the issue is returned to the original executor.
The active reviewer or approver transitions the issue to done with a comment:
PATCH /api/issues/{issueId}
{
"status": "done",
"comment": "Reviewed — implementation looks correct, tests pass."
}
The runtime determines whether this completes the workflow or advances to the next stage.
The active reviewer transitions to any non-done status with a comment:
PATCH /api/issues/{issueId}
{
"status": "in_progress",
"comment": "Button alignment is off on mobile. Please fix the flex container."
}
The runtime reassigns to the original executor automatically.
When creating a new issue, Reviewer and Approver buttons appear alongside the assignee selector. Clicking either opens a participant picker with:
Selections build the executionPolicy.stages array automatically.
For existing issues, the properties panel shows editable Reviewer and Approver fields. Multiple participants can be added per stage. Changes persist to the issue's executionPolicy via the API.