How Work Products and Artifacts Are Stored and Managed in Paperclip
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 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/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/master/server/src/services/work-products.ts). The workProductService encapsulates four core operations:
listForIssue(issueId)— Returns products ordered by primacy (primary first) and recencycreateForIssue(issueId, companyId, data)— Atomically clears any existing primary of the sametypebefore insertion, enforcing "last primary wins"update(id, patch)— Optionally promotes a product to primary within the same atomic transactionremove(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/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/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/master/skills/paperclip/references/artifacts.md):
- Upload bytes —
POST /api/companies/{companyId}/issues/{issueId}/attachmentsstores the file - Create work product — POST to
/api/issues/{id}/work-productswithtype: "artifact"andmetadata.attachmentIdreferencing the upload - Annotate — Set
title,summary,status,reviewStatefor review context - 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/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":
{
"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)
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
# 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_productswith comprehensive metadata and foreign keys - Logic:
workProductServiceinserver/src/services/work-products.tshandles CRUD and primary product atomicity - API: REST endpoints in
server/src/routes/issues.tsmirror 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_primaryflag 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →