Documentation

Everything you need to run Paperclip.

Guides, references, and walkthroughs for the people running AI agents at work. Start at the quickstart, or jump anywhere below.

Decision Training

Every day, the people in your company make judgement calls that agents can learn from — someone answers an agent's question in a thread, someone approves or rejects a risky action, someone decides how an execution stage should proceed. Decision training lets you keep those moments. You point at a decision that already happened, the server freezes everything the decider could see at that moment into an immutable snapshot, you add a note explaining why the call went the way it did, and the result becomes a labelled training example you can export.

Think of it as bookmarking good judgement. The snapshot is the input, your note plus the recorded outcome is the label.

Decision training examples are company-scoped, and company access is enforced on every request. If you are new to the API, read the API Overview first for base URL, authentication, and error-code conventions; this page builds on those and won't repeat them.

Writes are human-only. Creating, editing, and deleting examples require a signed-in human user on the board. An agent token calling those routes gets 403 Forbidden with { "error": "Decision training writes require a human user" }. This is deliberate — the whole point is to capture human judgement.


The three kinds of decision

When you capture an example you tell the server which kind of decision you are pointing at. There are exactly three:

sourceKind What it points at
interaction A question or request an agent raised on an issue thread, and how it was resolved.
approval An approval request on an issue — approved, rejected, or still pending.
execution_decision A decision recorded against an execution stage on an issue.

Whichever kind you choose, you also supply the sourceId of that specific decision and the issueId of the issue it lives on. All three must line up — the server looks the decision up scoped to both the company and the issue, so a mismatched pair comes back 404 Not Found with { "error": "Decision interaction not found" }, { "error": "Decision approval not found" }, or { "error": "Execution decision not found" } depending on the kind. An issueId that doesn't belong to the company returns { "error": "Issue not found" }.


What gets frozen: the snapshot

The most important idea here is the cutoff. The snapshot is not "the issue as it looks today" — it is "the issue as it looked at the instant the decision was made". Anything created after that moment is left out, so a training example can never accidentally contain the future.

For a resolved interaction or approval the cutoff is the moment it was resolved or decided. For an execution_decision it is the moment the decision was created. If the interaction or approval is still pending, there is no decision moment yet, so the cutoff is the capture time and decisionOutcome comes back null.

Everything at or before that cutoff is copied into a versioned snapshot object:

Snapshot field Contents
version Snapshot format version — currently 1.
retention The retention rules baked into this snapshot: policy, plus commentDeletion: "redact" and issueDeletion: "cascade".
capturedAt ISO timestamp of when the capture ran.
cutoff at (ISO timestamp), lastCommentId, and commentCount.
issue The full issue record.
comments Every issue comment created at or before the cutoff, oldest first.
runs The agent runs tied to this issue that had started and last updated at or before the cutoff.
decision kind, payload, actor, and outcome for the decision itself.
code Best-effort code context — see below.

Code context

Paperclip tries to record which code the decision was made against, so a training example can be replayed meaningfully. The code object carries repoUrl, ref, commitSha, and a resolution field telling you how confident that commit is:

resolution Meaning
exact The commit came from the exact run that produced the decision.
nearest_run No commit on the exact run, so the latest run before the cutoff that had one was used.
workspace Fell back to the execution or project workspace metadata.
none No commit could be determined.

Retention: what happens when the source changes

A snapshot is immutable, but it is not exempt from deletion requests. Two rules apply, and both are recorded inside every snapshot's retention block under the policy scrub_deleted_comments_v1:

  • Comment deletion redacts. If an issue comment that made it into a snapshot is later deleted, the server rewrites that comment inside the snapshot to an empty body, stamps deletedAt, and adds retentionRedaction with the reason source_comment_deleted. The rest of the snapshot is untouched.
  • Issue deletion cascades. If the issue is deleted, its decision training examples go with it.

Create an Example

POST /api/companies/{companyId}/decision-training

Capture a decision as a training example. Responds 201 Created with the stored example.

Request body:

Field Required Notes
sourceKind yes One of interaction, approval, execution_decision.
sourceId yes UUID of the decision.
issueId yes UUID of the issue the decision belongs to.
notes no Your explanation of the decision. Up to 100,000 characters. Defaults to "".

Unknown fields are rejected.

One example per decision, per person. The server enforces uniqueness on (sourceKind, sourceId, createdByUserId). If you have already trained this decision, a second attempt returns 409 Conflict with { "error": "This decision is already trained by this user" }. Your colleague can still train the same decision independently — the constraint is per author, so several people can label the same moment differently.

A successful create records a decision_training.created activity entry.

Example

curl -sS -X POST \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  "https://paperclip.example.com/api/companies/{companyId}/decision-training" \
  -d '{
    "sourceKind": "approval",
    "sourceId": "8f1c2b7e-5f3a-4d61-9a5e-2c0b7d4e1a93",
    "issueId": "3a9d0c14-77b2-4f8e-b0e6-9d5c1e2f7a44",
    "notes": "Rejected because the migration had no rollback path. Always ask for one on schema changes."
  }'

Preview an Example

POST /api/companies/{companyId}/decision-training/preview

Want to see exactly what would be frozen before you commit? Preview runs the same capture and returns the result without saving anything.

Request body takes sourceKind, sourceId, and issueId — the same three fields as create, minus notes. Unknown fields are rejected.

The response is:

{
  "cutoffAt": "2026-07-14T09:21:03.118Z",
  "decisionOutcome": "rejected",
  "snapshot": { "version": 1, "...": "..." }
}

Preview is read-only, but it carries the same human-only authorization as a write, because it exposes the same captured decision state.


List Examples

GET /api/companies/{companyId}/decision-training

Return the company's examples, newest first. Each row is { example, issueTitle, issueIdentifier }, so you can render a list without a second lookup per issue.

Query Parameters

Param Description
project Filter to examples whose issue belongs to this project (UUID).
kind Filter by source kind — interaction, approval, or execution_decision.
author Filter to examples created by a given user id.
q Case-insensitive search across the example's notes and the issue's title and identifier. Up to 500 characters.

An invalid query returns 400 Bad Request with { "error": "Invalid decision training query", "details": { ... } }.


Get an Example

GET /api/decision-training/{id}

Return one example by UUID, including its full snapshot, notes, and notesHistory. If the id is not a UUID, or the example belongs to a company you cannot access, the response is 404 Not Found with { "error": "Decision training example not found" } — the same answer either way, so the route never confirms an example you aren't allowed to see.

Reads are logged as decision_training.read activity.

Example fields

Field Meaning
id The example's UUID.
companyId Owning company.
sourceKind / sourceId The decision this example was captured from.
issueId The issue the decision lives on.
cutoffAt The moment the snapshot was frozen at.
notes The current note text.
notesHistory Prior note versions, each { author, at, body }.
decisionOutcome The recorded outcome, or null if the decision was still pending at capture.
retentionPolicy The retention policy applied — scrub_deleted_comments_v1.
snapshot The frozen state described above.
createdByUserId The human who captured it.
createdAt / updatedAt Timestamps.

Update the Notes

PATCH /api/decision-training/{id}

The snapshot never changes, but your explanation can. This route updates notes and nothing else.

Request body:

Field Required Notes
notes yes Up to 100,000 characters.

Unknown fields are rejected. Only the author can edit. Someone else attempting to change your example gets 403 Forbidden with { "error": "Only the example author can change decision training examples" }.

Old note text is never lost — each real change pushes the previous body onto notesHistory as { author, at, body } and logs a decision_training.notes_updated activity entry. Submitting the identical text is a no-op: the example is returned unchanged, with no history entry and no activity.


Delete an Example

DELETE /api/decision-training/{id}

Remove an example entirely. Responds 204 No Content with an empty body.

As with editing, only the author can delete, and only a human user can. A deletion records a decision_training.deleted activity entry.


Export as JSONL

GET /api/companies/{companyId}/decision-training/export.jsonl

This is the endpoint you point a training pipeline at. It returns the company's examples — the same set and order as the list route, newest first — as newline-delimited JSON, served with content type application/x-ndjson.

Each line is one complete JSON object with exactly three keys:

Key Contents
retentionPolicy The example's retention policy, e.g. scrub_deleted_comments_v1.
state The full frozen snapshot — the input side of the example.
label { "outcome": ..., "notes": ... } — the recorded decisionOutcome and the author's notes.

So a single line looks like this (formatted here for readability; in the file it is one line):

{
  "retentionPolicy": "scrub_deleted_comments_v1",
  "state": { "version": 1, "capturedAt": "...", "issue": {}, "comments": [], "runs": [], "decision": {}, "code": {} },
  "label": { "outcome": "rejected", "notes": "Rejected because the migration had no rollback path." }
}

The body ends with a trailing newline when there is at least one example, and is completely empty when there are none. Note that the export applies no filters — it always returns every example in the company. Use the list route when you want a filtered subset.

Every export is recorded as a decision_training.exported activity entry, including how many examples went out and their ids, so there is always an audit trail of who took the data.

Example

curl -sS \
  -H "Authorization: Bearer {token}" \
  "https://paperclip.example.com/api/companies/{companyId}/decision-training/export.jsonl" \
  -o decision-training.jsonl

Where to go next

  • Issues — the issues decision training examples hang off, and the source of the thread interactions you can capture.
  • Approvals — the approval requests you can capture with sourceKind: "approval".
  • Activity — where the decision_training.* entries described on this page show up.
  • API Overview — base URL, authentication, company scoping, and the shared error-code table.