# Tracking and Storing Work Products and Artifacts in Paperclip AI: A Complete Guide

> Learn to track and store work products and artifacts in Paperclip AI. Discover how Paperclip AI manages metadata in PostgreSQL and raw files in object storage with a unified API for seamless artifact management.

- Repository: [Paperclip/paperclip](https://github.com/paperclipai/paperclip)
- Tags: how-to-guide
- Published: 2026-08-12

---

**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`](https://github.com/paperclipai/paperclip/blob/main/scripts/paperclip-upload-artifact.sh). This script wraps a `POST` request to `/api/attachments` and automatically injects required `PAPERCLIP_*` environment variables.

```bash

# 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_KEY` and `PAPERCLIP_API_URL` from the environment
- **Content-type detection**: Uses `file` or 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/main/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:

```bash

# 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 pattern
- `workspaceKind`: Either `"execution_workspace"` or `"project_workspace"`
- `relativePath`: The file path relative to the workspace root
- `displayPath`: 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`](https://github.com/paperclipai/paperclip/blob/main/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`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/company-artifacts.ts):

- `classifyMediaKind(mimeType: string)` returns `"image"`, `"video"`, `"text"`, or `"file"` fallback
- `readTextAttachmentPreview(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`](https://github.com/paperclipai/paperclip/blob/main/company-artifacts.ts) service exports three helpers:

```typescript
// 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 issue
- **`root`** — 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`](https://github.com/paperclipai/paperclip/blob/main/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:

```tsx
// 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:

```bash

# 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.sh`](https://github.com/paperclipai/paperclip/blob/main/scripts/paperclip-upload-artifact.sh) for external files, or POST workspace references to `/api/issues/<task-id>/work-products` for in-repo artifacts
- **Unified metadata model**: The `issueWorkProducts` table stores both `attachmentId` and `resourceRef` variants, validated by `attachmentArtifactWorkProductMetadataSchema`
- **Rich presentation**: Automatic media classification, 4 KB text previews, and cursor-based pagination in [`server/src/services/company-artifacts.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/company-artifacts.ts)
- **Discourse integration**: Full-text search includes artifacts via [`company-search.ts`](https://github.com/paperclipai/paperclip/blob/main/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`](https://github.com/paperclipai/paperclip/blob/main/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`](https://github.com/paperclipai/paperclip/blob/main/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`](https://github.com/paperclipai/paperclip/blob/main/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.