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).
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.