The Issue Domain Model

All fields of the issues table: id, company_id, parent_id, title, description, status, assigned_agent_id, priority, metadata JSONB, created_at, updated_at. Field-by-field walkthrough.

The Issue Model: Paperclip's Work Queue

What you'll learn

Every field in the issues table, the six status states an issue moves through, and how issues relate to agents and heartbeat runs.

Issues Are the Unit of Work

In Paperclip AI everything an agent does is represented as an issue — a persistent row in the database that describes a task, tracks its lifecycle, and records the result. This is the central design decision: work is durable and auditable by default.

The Full issues Table

CREATE TABLE public.issues (
  id                  uuid        PRIMARY KEY DEFAULT gen_random_uuid(),
  company_id          uuid        NOT NULL REFERENCES companies(id),
  title               text        NOT NULL,
  description         text,
  status              text        NOT NULL DEFAULT 'open'
                                  CHECK (status IN ('open','claimed','in_progress','resolved','escalated','cancelled')),
  priority            text        NOT NULL DEFAULT 'medium'
                                  CHECK (priority IN ('low','medium','high','critical')),
  assigned_agent_id   uuid        REFERENCES agents(id),
  parent_issue_id     uuid        REFERENCES issues(id),    -- for subtasks
  result              text,                                  -- agent output
  result_structured   jsonb,                                 -- parsed result
  heartbeat_run_id    uuid        REFERENCES heartbeat_runs(id),
  attempt_count       int         NOT NULL DEFAULT 0,
  max_attempts        int         NOT NULL DEFAULT 3,
  created_at          timestamptz NOT NULL DEFAULT now(),
  claimed_at          timestamptz,
  resolved_at         timestamptz,
  due_at              timestamptz,
  metadata            jsonb
);

Status Lifecycle

open
 │
 ├─ claimed      (heartbeat has selected this issue; agent about to run)
 │
 ├─ in_progress  (adapter invoke called; waiting for result)
 │
 ├─ resolved     (agent returned output; work done)
 │
 ├─ escalated    (agent could not complete; requires human or manager review)
 │
 └─ cancelled    (manually cancelled; will not be retried)

The transition open → claimed → in_progress happens within a single heartbeat tick using the atomic checkout pattern (covered in detail in pcl-30).

Priority Queue Ordering

Issues are claimed in priority order within each heartbeat tick:

-- Simplified version of the heartbeat query
SELECT *
FROM issues
WHERE company_id = $1
  AND status = 'open'
  AND assigned_agent_id = $2
ORDER BY
  CASE priority
    WHEN 'critical' THEN 1
    WHEN 'high'     THEN 2
    WHEN 'medium'   THEN 3
    WHEN 'low'      THEN 4
  END,
  created_at ASC
LIMIT 1
FOR UPDATE SKIP LOCKED;

The parent_issue_id Field

Issues can have subtasks. Set parent_issue_id to create a hierarchy:

-- Parent: write a blog post
INSERT INTO issues (company_id, title, assigned_agent_id)
VALUES ('...', 'Write a blog post about PostgreSQL', '...')
RETURNING id;

-- Child tasks
INSERT INTO issues (company_id, title, parent_issue_id, assigned_agent_id)
VALUES
  ('...', 'Research: gather 5 key PostgreSQL facts',          'parent-id', 'researcher-id'),
  ('...', 'Outline: create 5-section blog outline',           'parent-id', 'planner-id'),
  ('...', 'Draft: write full blog post from outline',         'parent-id', 'writer-id');

The metadata Field

Store arbitrary structured data alongside an issue without schema changes:

UPDATE issues SET metadata = jsonb_build_object(
  'source',     'slack',
  'channel_id', 'C01234',
  'requester',  'alice@company.com',
  'tags',       ARRAY['blog', 'technical']
)
WHERE id = 'issue-id';

Checkpoint ✓

Sign in for progress tracking & Q&A →