# How Work Products and Artifacts Are Stored and Managed in Paperclip

> Discover how Paperclip stores and manages work products and artifacts in PostgreSQL. Access everything via a robust REST API for efficient data handling.

- Repository: [Paperclip/paperclip](https://github.com/paperclipai/paperclip)
- Tags: internals
- Published: 2026-08-16

---

**Paperclip stores every tangible deliverable as a first-class record in PostgreSQL, exposing it through a REST API that separates storage, business logic, and HTTP access.**

Work products and artifacts in the [paperclipai/paperclip](https://github.com/paperclipai/paperclip) repository represent the core mechanism for tracking what a task actually produced — whether that's a PDF report, a video walkthrough, a generated code bundle, or an internal planning document. Understanding how these are stored and managed is essential for integrating with the platform's review workflows.

## Database Model for Work Products

The foundation of work product storage is the `issue_work_products` table, defined in [[`packages/db/src/schema/issue_work_products.ts`](https://github.com/paperclipai/paperclip/blob/main/packages/db/src/schema/issue_work_products.ts)](https://github.com/paperclipai/paperclip/blob/master/packages/db/src/schema/issue_work_products.ts). This schema uses Drizzle ORM to map relational data with comprehensive foreign key relationships.

The table structure includes:

- **Relationship fields**: `id`, `company_id`, `project_id`, `issue_id`, `execution_workspace_id`, `runtime_service_id`, `created_by_run_id`
- **Descriptive fields**: `type`, `provider`, `external_id`, `title`, `url`, `status`, `review_state`, `is_primary`, `health_status`, `summary`, `metadata`, `source_trust`

These columns enable the UI to render work products, power review workflows, and calculate trust scores based on provenance.

## Service Layer: CRUD Operations with Primary Product Handling

All database interactions flow through [[`server/src/services/work-products.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/work-products.ts)](https://github.com/paperclipai/paperclip/blob/master/server/src/services/work-products.ts). The `workProductService` encapsulates four core operations:

- **`listForIssue(issueId)`** — Returns products ordered by primacy (primary first) and recency
- **`createForIssue(issueId, companyId, data)`** — Atomically clears any existing primary of the same `type` before insertion, enforcing "last primary wins"
- **`update(id, patch)`** — Optionally promotes a product to primary within the same atomic transaction
- **`remove(id)`** — Deletes the specified work product

The primary product mechanism uses the `is_primary` boolean to surface the most important artifact per `(issue, type)` pair. The service guarantees atomicity so UI consumers never see duplicate primaries.

## REST API Endpoints

The HTTP layer mirrors the service functions exactly. Routes in [[`server/src/routes/issues.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/routes/issues.ts)](https://github.com/paperclipai/paperclip/blob/master/server/src/routes/issues.ts) (lines 6797-7785) expose:

| Method | Endpoint | Service Function | Purpose |
|--------|----------|------------------|---------|
| GET | `/api/issues/:id/work-products` | `listForIssue` | List all products for an issue |
| POST | `/api/issues/:id/work-products` | `createForIssue` | Create validated via `createIssueWorkProductSchema` |
| PATCH | `/api/work-products/:id` | `update` | Modify metadata or toggle `isPrimary` |
| DELETE | `/api/work-products/:id` | `remove` | Remove a product |

The OpenAPI specification in [[`server/src/routes/openapi.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/routes/openapi.ts)](https://github.com/paperclipai/paperclip/blob/master/server/src/routes/openapi.ts) (lines 2271-2304) documents these contracts for client generation.

## The Artifact Upload Workflow

When a task produces a binary file requiring board inspection, Paperclip requires a strict two-step sequence documented in [[`skills/paperclip/references/artifacts.md`](https://github.com/paperclipai/paperclip/blob/main/skills/paperclip/references/artifacts.md)](https://github.com/paperclipai/paperclip/blob/master/skills/paperclip/references/artifacts.md):

1. **Upload bytes** — `POST /api/companies/{companyId}/issues/{issueId}/attachments` stores the file
2. **Create work product** — POST to `/api/issues/{id}/work-products` with `type: "artifact"` and `metadata.attachmentId` referencing the upload
3. **Annotate** — Set `title`, `summary`, `status`, `reviewState` for review context
4. **Close task** — Only after both steps succeed; comments alone don't constitute delivery

The helper script [[`skills/paperclip/scripts/paperclip-upload-artifact.sh`](https://github.com/paperclipai/paperclip/blob/main/skills/paperclip/scripts/paperclip-upload-artifact.sh)](https://github.com/paperclipai/paperclip/blob/master/skills/paperclip/scripts/paperclip-upload-artifact.sh) automates this flow for agent implementations.

## Workspace-Only Files: Direct Resource References

Not all deliverables require upload. For files that remain inside the execution checkout — such as generated Markdown plans — set `metadata.resourceRef.kind` to `"workspace_file"`:

```json
{
  "type": "document",
  "provider": "workspace",
  "title": "Regression test plan",
  "status": "ready_for_review",
  "reviewState": "needs_board_review",
  "metadata": {
    "resourceRef": {
      "kind": "workspace_file",
      "issueId": "<issue-id>",
      "workspaceKind": "execution_workspace",
      "workspaceId": "<workspace-id>",
      "relativePath": "doc/plans/regression-test-plan.md",
      "line": 1,
      "displayPath": "doc/plans/regression-test-plan.md"
    }
  }
}

```

This pattern lets the board open files directly without blob storage overhead. POST this JSON to `/api/issues/:id/work-products` using the same authentication headers.

## Code Examples: Creating Work Products

### CLI Method (Recommended for Agents)

```bash
npx paperclipai issue work-product:create $ISSUE_ID \
  --payload-json '{
    "type":"artifact",
    "provider":"paperclip",
    "title":"Design mockup",
    "status":"ready_for_review",
    "reviewState":"needs_board_review",
    "isPrimary":true,
    "metadata":{"attachmentId":"<uploaded-attachment-id>"}
  }'

```

### Raw cURL: Upload Then Register

```bash

# Step 1: Upload the file

curl -sS -X POST "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/issues/$PAPERCLIP_TASK_ID/attachments" \
  -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
  -H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
  -F 'file=@"walkthrough.webm";type=video/webm'

# Step 2: Extract attachmentId from response, then register work product

curl -sS -X POST "$PAPERCLIP_API_URL/api/issues/$PAPERCLIP_TASK_ID/work-products" \
  -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
  -H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
  -H "Content-Type: application/json" \
  --data-binary '{
    "type":"artifact",
    "provider":"paperclip",
    "title":"Walkthrough render",
    "status":"ready_for_review",
    "reviewState":"needs_board_review",
    "isPrimary":true,
    "metadata":{"attachmentId":"abc123"}
  }'

```

## Summary

- **Storage**: PostgreSQL table `issue_work_products` with comprehensive metadata and foreign keys
- **Logic**: `workProductService` in [`server/src/services/work-products.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/work-products.ts) handles CRUD and primary product atomicity
- **API**: REST endpoints in [`server/src/routes/issues.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/routes/issues.ts) mirror service operations
- **Binary artifacts**: Two-step workflow — upload to attachments endpoint, then create work product with `metadata.attachmentId`
- **Workspace files**: Use `metadata.resourceRef.kind: "workspace_file"` for direct file references without upload
- **Primary products**: `is_primary` flag with atomic "last wins" semantics per `(issue, type)` pair

## Frequently Asked Questions

### What distinguishes a work product from a regular attachment?

An attachment is raw blob storage; a work product is first-class metadata that registers a deliverable for review. According to the Paperclip source code, you must create a work product of type `"artifact"` referencing the attachment ID before the board recognizes delivery as complete. Attachments alone don't appear in review workflows.

### How does Paperclip prevent multiple primary products of the same type?

The `createForIssue` method in [`server/src/services/work-products.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/work-products.ts) executes an atomic transaction: it first updates any existing primary of the same `type` to `is_primary = false`, then inserts the new row with `is_primary = true`. This "last primary wins" rule guarantees at most one primary per `(issue, type)` pair without race conditions.

### Can I update a work product to make it primary after creation?

Yes. The `update` service method accepts a patch that may include `isPrimary: true`. The implementation atomically clears competing primaries of the same type before promoting the specified product, maintaining the same consistency guarantees as creation-time assignment.

### What authentication headers are required for work product API calls?

All endpoints require `Authorization: Bearer <api_key>`. For operations running inside a task execution, include `X-Paperclip-Run-Id: <run_id>` to attribute the action to the correct run context. The CLI and helper scripts handle header injection automatically.