Tracking and Storing Work Products and Artifacts in Paperclip AI: A Complete Guide
Paperclip AI treats every tangible output of an AI-agent task as a work product, storing metadata in PostgreSQL and raw file contents in object storage, with a unified API for uploading, referencing, listing, grouping, and searching artifacts.
Tracking and storing work products and artifacts in Paperclip AI ensures every output from an AI agent—documents, attachments, or generated files—remains discoverable, auditable, and accessible to board users and downstream tools. The control plane provides two primary patterns: uploaded attachments for files that leave the workspace, and workspace references for files that stay inside project repositories. This article walks through the implementation details, API endpoints, and source code patterns that power artifact management in the paperclipai/paperclip repository.
Uploading Deliverables with the Helper Script
The fastest way to create an artifact work product is through the bundled helper script scripts/paperclip-upload-artifact.sh. This script wraps a POST request to /api/attachments and automatically injects required PAPERCLIP_* environment variables.
# Upload a video artifact with the helper (runs inside a skill)
scripts/paperclip-upload-artifact.sh ./results/demo.webm \
--title "Demo walkthrough" \
--summary "Video walkthrough of the new feature"
The helper solves three problems for skill authors:
- Authentication: Pulls
PAPERCLIP_API_KEYandPAPERCLIP_API_URLfrom the environment - Content-type detection: Uses
fileor extension heuristics to set the correct MIME type - Metadata construction: Builds the work product payload with the returned
attachmentId
The complete API contract is documented in [skills/paperclip/references/artifacts.md](https://github.com/paperclipai/paperclip/blob/master/skills/paperclip/references/artifacts.md).
Workspace-Only References for In-Repo Artifacts
When a file must stay inside a project or execution workspace—such as a generated report committed to the repository—skills create a workspace file work product instead of uploading. This pattern avoids object storage costs and keeps sensitive files within your existing access controls.
The payload requires a metadata.resourceRef object describing the workspace location:
# Create a workspace-only artifact (JSON payload saved as work-product.json)
cat > workspace-file-work-product.json <<'EOF'
{
"type": "document",
"provider": "workspace",
"title": "Regression test plan",
"status": "ready_for_review",
"reviewState": "needs_board_review",
"summary": "Markdown plan committed in the execution workspace.",
"metadata": {
"resourceRef": {
"kind": "workspace_file",
"issueId": "$PAPERCLIP_TASK_ID",
"workspaceKind": "execution_workspace",
"workspaceId": "$PAPERCLIP_RUN_ID",
"relativePath": "doc/plans/regression-test-plan.md",
"displayPath": "doc/plans/regression-test-plan.md"
}
}
}
EOF
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 @workspace-file-work-product.json
Key fields in resourceRef:
kind: Always"workspace_file"for this patternworkspaceKind: Either"execution_workspace"or"project_workspace"relativePath: The file path relative to the workspace rootdisplayPath: Human-readable path shown in the UI
Metadata Model and Database Schema
All work products land in the issueWorkProducts table with type = 'artifact' and provider = 'paperclip'. The JSON metadata column holds one of two structures validated by attachmentArtifactWorkProductMetadataSchema in server/src/services/company-artifacts.ts:
| Storage Pattern | Metadata Structure |
|---|---|
| Uploaded attachment | { "attachmentId": "uuid-string" } |
| Workspace reference | { "resourceRef": { ... } } |
The schema validation enforces type safety at the service layer, preventing malformed work products from entering the database.
Classification and Preview Generation
When listing artifacts, the service classifies media kinds from content-type headers and generates previews for text-based formats. In server/src/services/company-artifacts.ts:
classifyMediaKind(mimeType: string)returns"image","video","text", or"file"fallbackreadTextAttachmentPreview(attachmentId: string)reads the first 4 KB from storage and normalizes markdown for safe rendering
This preview logic runs during list operations so the UI can show snippet cards without additional round-trips.
Pagination with Cursor-Based Navigation
Artifact collections use cursor pagination to handle large result sets efficiently. The company-artifacts.ts service exports three helpers:
// From server/src/services/company-artifacts.ts
encodeCursor({ updatedAt, id }) // → base64-encoded string
decodeCursor(cursor) // → { updatedAt, id }
cursorCondition(cursor) // → Prisma WHERE clause
The cursor encodes the last item's updatedAt timestamp and id to ensure stable ordering even when new records arrive between requests.
Grouping Artifacts by Task or Root Issue
The groupBy query parameter enables two aggregation modes:
task— Groups artifacts by their immediate parent issueroot— Groups by the top-level issue in the hierarchy
The buildArtifactGroups function (same service file) aggregates counts, collects media kinds, selects preview artifacts, and generates view URLs like /artifacts?groupBy=root&rootIssueId=xxx.
Search Integration
Artifacts surface in Paperclip's full-text search through server/src/services/company-search.ts. The search indexer includes:
- Artifact titles and summaries
- Preview text from the first 4 KB of text files
- Related issue data (task names, descriptions)
This ensures work products appear alongside tasks and other entities when users search their workspace.
Fetching and Rendering in the React UI
Client applications consume the artifact API directly. Here's a minimal React component pattern from the codebase:
// React UI component – fetch and render artifact cards
import { useEffect, useState } from "react";
import { Card } from "./artifacts/ArtifactCard";
function ArtifactList({ companyId }: { companyId: string }) {
const [artifacts, setArtifacts] = useState<CompanyArtifact[]>([]);
useEffect(() => {
fetch(`/api/companies/${companyId}/artifacts?limit=20`, {
headers: { Authorization: `Bearer ${process.env.PAPERCLIP_API_KEY}` },
})
.then((r) => r.json())
.then((data) => setArtifacts(data.artifacts));
}, [companyId]);
return (
<div className="grid gap-4">
{artifacts.map((a) => (
<Card key={a.id} artifact={a} />
))}
</div>
);
}
For direct API access without the React layer:
# List the latest 10 artifacts for a company (REST call)
curl -sS "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/artifacts?limit=10" \
-H "Authorization: Bearer $PAPERCLIP_API_KEY"
Summary
- Two storage patterns: Use
scripts/paperclip-upload-artifact.shfor external files, or POST workspace references to/api/issues/<task-id>/work-productsfor in-repo artifacts - Unified metadata model: The
issueWorkProductstable stores bothattachmentIdandresourceRefvariants, validated byattachmentArtifactWorkProductMetadataSchema - Rich presentation: Automatic media classification, 4 KB text previews, and cursor-based pagination in
server/src/services/company-artifacts.ts - Discourse integration: Full-text search includes artifacts via
company-search.ts, and React components consume the REST API directly
Frequently Asked Questions
What is the difference between an uploaded attachment and a workspace file in Paperclip AI?
An uploaded attachment stores the raw file bytes in Paperclip's object storage service and creates an artifact work product with an attachmentId. A workspace file reference only stores metadata pointing to a file that remains in your project or execution workspace, keeping sensitive data within your existing infrastructure. Choose workspace references for files already version-controlled in your repository.
How does cursor pagination work for artifact lists?
The cursor encodes the updatedAt timestamp and id of the last item in the current page. The encodeCursor, decodeCursor, and cursorCondition functions in server/src/services/company-artifacts.ts manage this transparently. Pass the returned nextCursor to subsequent requests to traverse the collection without offset-based skipping.
Can I search inside artifact contents, not just titles?
Yes. The company-search.ts service indexes preview text from text-based artifacts (first 4 KB normalized) alongside titles and summaries. Binary files like images and videos are searchable by metadata only. The search index updates asynchronously as artifacts are created or modified.
What environment variables does the upload helper require?
The paperclip-upload-artifact.sh script requires PAPERCLIP_API_URL, PAPERCLIP_API_KEY, PAPERCLIP_TASK_ID, and PAPERCLIP_RUN_ID in the environment. These are automatically injected when running inside a Paperclip skill execution environment. For local testing, export them manually or use a .env file loaded by your shell.
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 →