Goals Hierarchy and Goal-to-Task Linking Data Model in Paperclip
Paperclip implements a flexible three-level goals system using a single self-referencing table with parent_id relationships and join tables for project associations.
The open-source Paperclip platform models work planning through a unified goals architecture that supports arbitrary nesting of projects, goals, and tasks. This article examines the complete data model, schema definitions, and API patterns used to implement hierarchical goal management and goal-to-task linking in the paperclipai/paperclip repository.
Core Goals Table Schema
The foundation of Paperclip's hierarchy lives in packages/db/src/schema/goals.ts. This Drizzle ORM schema defines a single table that stores all three logical levels—projects, goals, and tasks—differentiated by a level column rather than separate tables.
Key Columns and Their Roles
| Column | Type | Purpose |
|---|---|---|
id |
UUID | Primary key for all goal entities |
company_id |
UUID | Multi-tenant isolation boundary |
title |
string | Display name |
description |
text | Detailed context |
level |
enum ("project", "goal", "task") |
Logical type discriminator |
status |
enum ("planned", "active", "completed") |
Lifecycle state |
parent_id |
UUID (nullable, self-referencing FK) | Hierarchical parent link |
owner_agent_id |
UUID (nullable) | Assignment to AI or human agent |
created_at / updated_at |
timestamps | Audit trail |
The parent_id column creates the tree structure. Any goal row can reference another row in the same table, enabling unlimited nesting depth. The level column defaults to "task" and provides semantic categorization without constraining the actual hierarchy.
Goal Hierarchy Implementation
Paperclip's nesting mechanism relies entirely on the self-referencing foreign key pattern. This design choice keeps queries simple while supporting complex tree operations.
Creating Hierarchical Goals
// Create a top-level project
const project = await api.post('/goals', {
title: 'Launch New Marketing Campaign',
description: 'Define strategy, create assets, and run ads.',
level: 'project',
status: 'planned',
companyId: currentCompany.id,
});
// Nest a goal under the project using parentId
const goal = await api.post('/goals', {
title: 'Develop Campaign Copy',
description: 'Write headlines and ad copy.',
level: 'goal',
status: 'planned',
parentId: project.id, // Hierarchical link established here
companyId: currentCompany.id,
});
// Create a task under the goal
const task = await api.post('/goals', {
title: 'Write Facebook Ad Text',
level: 'task',
status: 'planned',
parentId: goal.id,
companyId: currentCompany.id,
});
The level field is advisory—schema validation in server/src/services/goals.ts enforces business rules, but the database permits any level value at any nesting depth. This flexibility allows edge cases like project-within-project structures when business needs demand it.
Project-to-Goal Linking via Join Table
Goals exist independently of projects. The many-to-many relationship between projects and goals is managed through packages/db/src/schema/project_goals.ts:
// project_goals.ts schema structure
{
project_id: UUID, // references projects.id
goal_id: UUID, // references goals.id
}
This separation enables goal reusability: a single milestone goal can belong to multiple projects without duplication, and goals can exist in draft state before project assignment.
Linking a Goal to a Project
await api.post('/project-goals', {
projectId: 'a1b2c3d4-5678-90ab-cdef-0987654321fe',
goalId: goal.id,
});
The server/src/routes/goals.ts handler processes these associations, with validation logic in server/src/services/goals.ts ensuring company-scoped access control.
Fetching Hierarchical Data
Paperclip's API supports eager loading of child goals through a query parameter pattern:
const hierarchy = await api.get(
`/projects/${projectId}/goals?includeChildren=true`
);
// Returns nested structure:
// {
// id: "...",
// title: "Launch New Marketing Campaign",
// level: "project",
// children: [
// {
// id: "...",
// title: "Develop Campaign Copy",
// level: "goal",
// children: [
// { id: "...", title: "Write Facebook Ad Text", level: "task", children: [] }
// ]
// }
// ]
// }
The service layer in server/src/services/goals.ts implements recursive CTEs or multiple query batching to construct this tree efficiently. The ui/src/api/goals.ts client unwraps the nested response for React component consumption.
Status and Ownership Tracking
Each goal carries status and assignment metadata that propagate through the hierarchy:
status: Drives workflow automation and UI state. Valid transitions are enforced in the goals service layer.owner_agent_id: Links to theagentstable, supporting both human users and autonomous AI agents as goal owners.
These fields enable aggregate reporting—project completion percentages roll up from leaf task statuses through parent goals.
Key Source Files
Understanding the complete implementation requires examining these files in paperclipai/paperclip:
| File | Responsibility |
|---|---|
packages/db/src/schema/goals.ts |
Drizzle schema definition for the goals table with hierarchy fields |
packages/db/src/schema/project_goals.ts |
Join table schema for project-goal associations |
server/src/services/goals.ts |
Business logic: CRUD, validation, hierarchy queries, status transitions |
server/src/routes/goals.ts |
Express route handlers exposing /api/goals endpoints |
ui/src/api/goals.ts |
Frontend TypeScript client for goal API consumption |
docs/api/goals-and-projects.md |
Public API contract documentation |
Summary
- Single-table inheritance: The
goalstable stores projects, goals, and tasks differentiated bylevel. - Self-referencing hierarchy:
parent_idcreates unlimited nesting depth without separate tables per level. - Decoupled project association: The
project_goalsjoin table enables many-to-many relationships. - Service-layer validation:
server/src/services/goals.tsenforces business rules on hierarchy depth, status transitions, and access control. - Recursive fetching:
includeChildren=trueparameter loads complete goal trees in one round-trip.
Frequently Asked Questions
How does Paperclip distinguish between a project, goal, and task?
The level column in the goals table uses string values ("project", "goal", "task") to semantically categorize each row. This is not enforced at the database level—parent_id relationships control actual hierarchy—so the service layer validates that business rules match intended use.
Can a goal belong to multiple projects?
Yes. The project_goals join table implements a many-to-many relationship. Inserting multiple rows with the same goal_id and different project_id values links one goal to several projects simultaneously.
What happens when I delete a parent goal?
The schema uses a standard foreign key on parent_id. Depending on migration settings, deletion either cascades to child goals or is blocked until children are reassigned. Check packages/db/src/schema/goals.ts for the specific onDelete behavior in your deployment.
How do I query all tasks under a specific project?
Use the hierarchical fetch endpoint with includeChildren=true, then filter client-side by level: "task". For server-side filtering, extend the query in server/src/services/goals.ts to accept a level parameter that constrains the recursive CTE.
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 →