Custom Fields (Developer)
This guide explains how custom fields are defined, stored, and managed at the API level. For the user-facing documentation, see the Custom Fields user guide.
Field Definition Schema
Each board has a fields array defining the available field types. A field definition looks like this:
interface FieldDefinition {
id: string; // Unique identifier (e.g., "fld_abc123")
name: string; // Display name (e.g., "Priority")
type: FieldType; // One of the supported field types
required: boolean; // Whether the field is required
defaultValue?: any; // Default value for new items
description?: string; // Help text
options?: FieldOption[]; // For select-type fields
config?: Record<string, any>; // Type-specific configuration
}
Supported Field Types
type FieldType =
| "short_text"
| "long_text"
| "email"
| "url"
| "phone"
| "number"
| "currency"
| "percentage"
| "rating"
| "status"
| "single_select"
| "multi_select"
| "priority"
| "date"
| "datetime"
| "timeline"
| "duration"
| "person"
| "people"
| "checkbox"
| "file"
| "label"
| "formula"
| "dependency"
| "mirror"
| "auto_number"
| "created_at"
| "updated_at";
Storage
Field values are stored as a JSONB column on the items table:
CREATE TABLE items (
id TEXT PRIMARY KEY,
board_id TEXT NOT NULL REFERENCES boards(id),
group_id TEXT NOT NULL REFERENCES groups(id),
title TEXT NOT NULL,
description TEXT,
fields JSONB NOT NULL DEFAULT '{}',
position INTEGER NOT NULL DEFAULT 0,
created_by TEXT NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
The fields JSONB column stores a key-value map where keys are field IDs and values depend on the field type:
{
"fld_status": "in_progress",
"fld_priority": "high",
"fld_assignee": "usr_abc123",
"fld_tags": ["backend", "auth"],
"fld_duedate": "2026-03-25",
"fld_estimate": 8,
"fld_done": true
}
Adding Fields via API
board.addField
POST /trpc/board.addField
{
"boardId": "brd_abc123",
"name": "Estimate",
"type": "number",
"required": false,
"config": {
"unit": "hours",
"min": 0,
"max": 100
}
}
board.updateField
POST /trpc/board.updateField
{
"boardId": "brd_abc123",
"fieldId": "fld_abc123",
"name": "Story Points",
"config": {
"unit": "points"
}
}
board.removeField
POST /trpc/board.removeField
{
"boardId": "brd_abc123",
"fieldId": "fld_abc123"
}
Removing a field deletes the field definition and removes the corresponding key from all items' fields JSONB.
Formula Fields
Formula fields compute their value based on other fields. The formula syntax supports:
{fld_estimate} * {fld_rate} // Arithmetic
IF({fld_status} = "done", "Yes", "No") // Conditionals
DAYS_BETWEEN({fld_start}, {fld_end}) // Date functions
SUM({sub_items.fld_estimate}) // Aggregation over sub-items
Formula values are computed on read, not stored. They are recalculated each time the item is fetched.
Indexing
For frequently filtered fields, PostgreSQL GIN indexes are created on the JSONB column:
CREATE INDEX idx_items_fields ON items USING GIN (fields);
CREATE INDEX idx_items_status ON items ((fields->>'fld_status'));
This ensures efficient queries even with large numbers of items and complex filters.