workspace-scoped custom priorities
Motivation
Task priorities are currently a fixed set of five hardcoded values — no-priority, low, medium, high, urgent — defined in apps/api/src/task/validate-task-fields.ts and mirrored in eight or more places across the frontend, API, i18n, and integration plugins. Teams that want "Critical", "Minor", "P0…P3", or an internal priority scheme have no way to express that without patching the source.
The ask is to let each workspace define its own priority list — similar to how labelTable already works for workspace-scoped labels.
Current state of the code
Priority is just a text column on task (apps/api/src/database/schema.ts) with a default of \"low\". The value is validated at runtime against a hardcoded constant and rendered by a handful of components that each know the five possible values.
Locations where the priority enum lives (API):
apps/api/src/task/validate-task-fields.ts—VALID_PRIORITIESconstant,assertValidPriority,coercePriorityapps/api/src/schemas.ts— ValibotpicklistintaskSchemaapps/api/src/task/controllers/get-tasks.ts— SQLCASEexpression for priority sort orderapps/api/src/task/controllers/create-task.ts,update-task-priority.ts,import-tasks.tsapps/api/src/plugins/github/events/task-priority-changed.ts+utils/format.tsapps/api/src/plugins/gitea/events/task-priority-changed.ts
Locations on the frontend:
apps/web/src/components/task/task-priority-popover.tsx— hardcodedpriorityOptionsarrayapps/web/src/components/board/board-toolbar.tsx— hardcoded priority list in filtersapps/web/src/components/bulk-selection/backlog-bulk-toolbar.tsx— memoized priority optionsapps/web/src/lib/priority.tsx—getPriorityIconswitchapps/web/src/lib/i18n/domain.ts—getPriorityLabelapps/web/src/hooks/use-task-filters.ts— in-memory priority filter
i18n: tasks.priority namespace in all nine locales (i18n/*.json).
Proposed approach
Option A — just add more built-in priorities (not what was asked, but cheap)
Extend VALID_PRIORITIES, the Valibot picklist, the SQL sort CASE, and the eight frontend/i18n touch-points. 1–2 days. This is listed for completeness; it does not actually solve the request.
Option B — workspace-scoped custom priorities (recommended, what was asked)
Schema
New table modelled loosely on labelTable:
export const priorityTable = pgTable(\"priority\", {
id: text(\"id\").$defaultFn(() => createId()).primaryKey(),
workspaceId: text(\"workspace_id\").notNull().references(() => workspaceTable.id, {
onDelete: \"cascade\",
onUpdate: \"cascade\",
}),
name: text(\"name\").notNull(),
color: text(\"color\"),
order: integer(\"order\").notNull().default(0),
isDefault: boolean(\"is_default\").default(false).notNull(),
createdAt: timestamp(\"created_at\", { mode: \"date\" }).defaultNow().notNull(),
updatedAt: timestamp(\"updated_at\", { mode: \"date\" }).defaultNow().$onUpdate(() => new Date()).notNull(),
}, (table) => [
index(\"priority_workspaceId_idx\").on(table.workspaceId),
unique(\"priority_workspace_name_unique\").on(table.workspaceId, table.name),
]);
taskTable.prioritychanges fromtext(\"priority\")totext(\"priority_id\").references(() => priorityTable.id, { onDelete: \"set null\" }).- Migration needs to:
- Create
priorityTable. - For every existing workspace, insert five default rows matching
no-priority / low / medium / high / urgent(with sensibleorderandisDefaultformediumor whatever the team picks). - Backfill
task.priority_idfrom the currenttask.prioritytext value → matching new row id. - Drop the old text column.
- Create
Backend
- New CRUD endpoints under
apps/api/src/priority/(GET/POST/PATCH/DELETE by workspace). Follow the same shape as the existinglabelroutes. assertValidPrioritybecomes async and takes the workspace id.get-tasks.tsneeds a new priority sort approach — eitherORDER BY priority.ordervia join, or fetch the priority list for the project's workspace once per request and build a dynamicCASE.taskSchemainschemas.tsloses the Valibot picklist; priority becomesnullable(string)(the id).- GitHub/Gitea plugins need updating — they translate our internal priority values to provider-specific labels, and that mapping now needs to go through a user-defined name instead of a fixed enum.
Frontend
- New query hook
useGetPriorities(workspaceId)with TanStack Query. - Replace the hardcoded arrays in
task-priority-popover.tsx,board-toolbar.tsx,backlog-bulk-toolbar.tsxwith data from the query. getPriorityIconbecomesgetPriorityIcon(priority)wherepriorityis a row frompriorityTable— render a colored dot or a user-provided icon.- New settings screen in the workspace settings area to create/reorder/delete priorities.
- i18n strings become display strings for the settings UI only — priority names themselves are user data, not translated.
Risk / scope
- 2–3 weeks of work for one person.
- Moderate risk: the async validator change ripples through every task mutation controller, and the migration needs to be careful to avoid data loss on busy instances.
- Backward compatibility for API consumers: API responses change from
priority: \"high\"topriority: { id, name, color, order }(or justpriorityId+ separate lookup). This is a breaking change for any external API client.
Open questions
- Should the five existing values be "locked" defaults that every workspace starts with and cannot delete, or fully user-editable including the seeded rows?
- What happens to a task whose priority is deleted?
ON DELETE SET NULLmakes it "no priority" — acceptable? - Do we want a workspace-level "default priority for new tasks" setting (replaces the current
default(\"low\")behaviour)? - Export/import (
export-tasks.ts,import-tasks.ts): do we export priority names (portable across workspaces) or ids (breaks on import into a different workspace)? - How do the GitHub and Gitea integrations handle custom priorities that don't map to any provider-native label?
Acceptance criteria
- A workspace admin can create, rename, recolor, reorder, and delete priorities from workspace settings.
- Every task creation, update, and filter UI reads priorities from the workspace instead of a hardcoded list.
- Existing tasks migrate without data loss and existing workspaces end up with the same five priorities they had before.
- Board sort-by-priority respects the workspace-defined
order. - GitHub and Gitea integrations degrade gracefully for priorities they don't recognize.
- Import/export round-trips priorities in a predictable way (decision documented in the PR description).
Related
- Feature: #1166 (task author sticker — similar task-metadata surface, unrelated scope)
Originally requested by @RainyPixel on 2026-04-07. Original GitHub request #1171.
ROA-804
Original GitHub comment