Labels & goal views
Task labels are free-form tags (string[], GIN-indexed, default []) that are not project-scoped. That makes them Project Tracker's lightest cross-project grouping key: a label such as q3-launch used across several projects behaves like a "goal that spans projects" without any new entity. (The heavier, structured cross-project constructs are cross-project epics (PT-446) and cross-cutting initiatives (PT-447).)
Three surfaces read labels, each with a deliberately different authorisation bar.
Cross-project label goal view (PT-448)
GET /api/tasks?labels=<label> lists every task carrying any of the requested labels across every project the caller can read (tenant-scoped). It requires full tasks.read (the list is not ownership-scoped, so a tasks.read.own-only caller is rejected — mirrors the per-project full task list) and projects.read (or .own) plus an active membership; a GUEST member or a projects.read.own-only caller is restricted to collaborator/creator projects. The labels param is required and bounded (at most 20 labels, each ≤ 100 characters).
Access is enforced two ways: tasks in projects the caller cannot read are omitted at the query level (the readable-project set narrows the query, on top of RLS), and any cross-project dependency reference returned on a row is redacted — taskKey and projectId are omitted and title/status are replaced with the redactCrossProjectDeps sentinels ((cross-project dependency) / UNKNOWN), with isCrossProject: true, so only the opaque id survives unchanged and the real key/title never leak (PT-283 precedent). The blocked/blocking indicator is exposed as booleans (isBlocked / isBlocking) derived server-side from same-project TaskLink BLOCKS links and incomplete dependencies, so it never leaks a linked task's identity.
Each row carries { project, taskKey, title, status, priority, isBlocked, isBlocking, dependsOn, dependedOnBy } and is sorted by priority (URGENT first), task key as the tiebreak. Results are capped at the 500 highest-priority rows (the cap is applied after a priority ranking pass, so urgent work is never dropped in favour of lower-priority rows); the response envelope carries truncated: true when more match. This is a fail-safe for high-cardinality labels — full cursor pagination is deferred to the PT-206 board. The goal view is surfaced in PT V2 at /goals?label=<label> — bookmarkable and shareable, since a label is effectively a goal URL.
Label suggestions (PT-482)
GET /api/tasks/labels returns the distinct labels in use for the tenant, most-used first — the autocomplete vocabulary behind the Jira-style label combobox (type-ahead + create-new) on the task-detail editor, the task popup, and the create-task modal. The aggregation is unnest(labels) + COUNT(DISTINCT task_id) (a task is counted once even if its labels array repeats a value) ordered by count desc then label asc, run under the tenant RLS context.
Query params (all optional): projectId (UUID — narrows to one project; omitted = tenant-wide), q (case-insensitive prefix filter, ≤ 100 chars), and limit (clamped to [1, 200], default 50). The response is { data: { labels: [{ label, count }] } }.
Authorisation is deliberately lighter than the goal view: an active member who can read or create tasks — any scope satisfies it, including the .own variants (tasks.read.own / tasks.create.own). The response is non-sensitive autocomplete strings (never task content), so it doesn't need the goal view's full-tasks.read gate.
Task-list label filter + unified filter axes (PT-483)
Both Tasks-page surfaces — the org-wide /tasks page and the project-scoped /projects/:id/tasks page — render the same filter set, fed by /api/my-tasks (the project page pins its projectId). Two changes keep them from drifting:
- Labels filter.
GET /api/my-tasksaccepts?labels=<a,b>(comma-separated, OR semantics, ≤ 20 labels of ≤ 100 chars each — the sameparseLabelsParambounds as the per-project and goal-view routes). Labels are a content field, not a visibility-leak vector, so the filter is honoured in bothmode=allandmode=mine. It is task-only: the issue branch returns nothing when the filter is active, in both the Prisma and raw-SQL query paths. - Label options endpoint.
GET /api/task-labelsreturns the distinct, sorted set of task labels for populating the filter chip (mirrors/api/issue-tagsfor the Issues Tag axis), scoped to exactly the tasks the caller can list via/api/my-tasks— distinct from the lighter, most-used-firstGET /api/tasks/labelsautocomplete vocabulary above (PT-482), which feeds the label editor tenant-wide. The filter-chip endpoint uses the collaborator-or-creator project window for GUEST /projects.read.own-only callers, and — fortasks.read.own-only callers — pins to their own assigned tasks, so the dropdown never exposes a label the caller's task list could not match. - Collaborators on both pages. The Collaborators filter (PT-471), previously only on the org-wide page, now also appears on the project page, which forwards
collaboratorIdsto/api/my-tasks(honoured inmode=all). - Reporter filter (PT-1058).
GET /api/my-tasksaccepts?reporterIds=<uuid,uuid>(OR semantics; malformed ids are dropped). It matches the current reporter,Task.reporterIdon tasks andIssue.reportedByon issues, so unlike the task-only axes it keeps issues. It is honoured in both modes, like labels, because it narrows rows the caller can already see. The reporter defaults to the user who filed the item but can be edited, so it answers "who filed this" only while nobody has changed it. A task with no reporter matches no selection. The chip is labelled Reporter, sits next to Assignee, and persists under thereporterURL key.
Defects: classify with a label, not the type (PT-1033)
Mark a task that remediates a defect with a label, whether or not it has a parent, and build
a defect queue on that label. issueType: BUG alone cannot do the job. (A defect report nobody is
fixing yet is an issue, not a task; issues carry no labels, and have their own tags instead.)
The type fails because a task with a parent is a SUBTASK (the type/parent
contract): parenting a BUG overwrites its type, and a
request that asks for BUG on a parented task is refused rather than applied. A queue keyed on
issueType=BUG therefore under-reports by construction — every defect filed under a container
drops out of it. A label is orthogonal to the hierarchy, so it survives parenting and detaching
alike.
Which label is a workspace convention, not a product rule. Constellation's own workspace uses
kind:bug, the kind: axis of its backlog taxonomy (area: / kind: / triage:); the examples
below use it.
Querying the queue. Parented defects are subtasks, and the per-project list endpoints return top-level tasks only unless told otherwise:
| Surface | Includes subtasks |
|---|---|
Goal view — /goals?label=kind:bug (GET /api/tasks?labels=) | Yes — every project the caller can read, capped at 500 rows (truncated: true past it) |
Tasks page label filter (/api/my-tasks?labels=) | Yes, except in the Parents roadmap view, which lists containers only |
Per-project list (GET /api/projects/:id/tasks?labels=) | Only with parentTaskId=all |
MCP list_tasks / pt list-tasks | Only with parentTaskId: "all" / --parent all |
Defects filed before a workspace adopts the label may carry issueType=BUG and no label. Until
they are labelled, the queue is the union of two separate queries — labels=kind:bug and
issueType=BUG, both with parentTaskId=all on the per-project surfaces. Passing both filters in
one request ANDs them, and the flow filters (ready / review / awaiting_decision) ignore
them. The union still misses a defect that was parented before it was labelled: parenting made it
a SUBTASK, so neither query finds it. Where a REPARENT audit row exists it records the type the
task had before (see the type/parent contract); that row
is the only place to recover it from.
Labels are a replace-the-set field on PATCH: read the current labels and send them back with the
new one added, or the others are dropped.
See also
- Work hierarchy & links — the structured cross-project constructs (epics, initiatives).
- Tasks page — the unified work surface where the filters render.
- Project Tracker module — the module landing page.