Labels
View on GitHubLabels
Issue labels are a per-project resource backed by a real
labels database table (App\Models\Label) — not a fixed backend
enum. Every project gets a starter taxonomy of six labels (bug,
feature, performance, design, ux, chore) seeded lazily the
first time they're needed, and project owners/admins can rename,
recolor, describe, add, or delete any label — including the seeded
ones — from Settings → Labels.
Guides, in the order you'd actually need them
- Add a new default labelLabelsAdd a new default labelWorked example: adding a seventh starter label, security, to the six every project is seeded with (bug, feature, performance, design, ux, chore). This only… —
worked example adding a seventh starter label,
security, to the taxonomy every new project is seeded with. - Expose project labels to a new pageLabelsExpose project labels to a new pageWorked example: giving a brand-new, single-project page real label colors (instead of useProjectLabels()'s hashed fallback) by mounting a ProjectLabelsProv… —
worked example wiring real per-project label colors (instead of the
hashed fallback) into a brand-new single-project page, using a
hypothetical
Backlogpage in the exact shape of the existingProjects/Showpage.
The architecture in one paragraph
labels (migration 2026_09_12_120000_create_labels_table.php) has
project_id, name, color, description, is_system, unique on
(project_id, name). App\Repositories\LabelRepository and
App\Services\LabelService follow this codebase's usual
Controller → Service → Repository split (see
../architecture/02-backend-layered-architecture.mdArchitecture & project technicalsBackend layered architectureController → Service → Repository, with no exceptions and no shortcuts — a controller never builds an Eloquent query, and a repository never contains busin…):
LabelService::ensureSystemLabels() seeds the six starter labels
exactly once per project, tracked by a projects.labels_seeded_at
timestamp — not "insert whatever's missing" on every read, which would
silently resurrect a label a project owner deliberately deleted.
App\Http\Controllers\LabelController exposes
projects.labels.store / .update / .destroy, gated by
App\Policies\ProjectPolicy::viewLabels() / manageLabels() (view:
owner/admin/member/viewer; manage: owner/admin — the same
owner/admin-only pattern as viewIntegrations()/updateIntegrations()).
An issue's labels are rows of the issue_label pivot
(issue_id, label_id; migration
2026_10_17_100000_create_issue_label_table.php, which also moved the
old JSON names over and dropped issues.labels), exposed as
Issue::labels() (BelongsToMany). Writers still speak names:
assigning labels on an Issue (create(), update(), factories,
imports, templates, automations) goes through
Issue::setLabelsAttribute() and is written to the pivot by
Issue::save(); names that are not a label of the issue's project are
ignored, and every name written through
IssueController@store/@update is validated against that project's
real labels rows
(Rule::exists('labels', 'name')->where('project_id', ...)). Reads
serialise labels as a list of names (Issue::relationsToArray()), and
listing queries must eager-load labels. Because storage is relational,
filtering is by label id (?labels=1,2 →
whereHas('labels', ... whereIn('labels.id', ...)), an exact match — a
bug filter never catches debug) and renaming a label shows the new
name on every issue at once; deleting one cascades its pivot rows.
Label bumps CacheVersion::ISSUES on save/delete so cached issue lists
refresh. The few places that still keep a label as plain text (template
default_labels, Jira label mappings, add/remove-label automation
params) are rewritten or cleaned up by
LabelRepository::replaceNameReferences() when a label is renamed or
deleted. On the frontend,
resources/js/context/ProjectLabelsContext.tsx's
ProjectLabelsProvider/useProjectLabels() is the single source of
truth for "what labels exist and what color is this one" wherever it's
mounted (currently Pages/Projects/Show.tsx and Pages/Issues/Show.tsx,
each fed by a labels prop their controller builds with a small
private mapLabels() helper) — LabelBadge, EditableLabelList, and
the labels FilterDropdown all read from it instead of a hardcoded
list, and useProjectLabels().getColor(name) falls back to
hashLabelColor() (a deterministic hash into LABEL_COLOR_PALETTE in
resources/js/utils/labelColors.ts) when no provider is mounted or the
name isn't found — which is exactly what happens today on the
cross-project Dashboard page. The Settings → Labels tab itself
(WorkspaceSettingsLabelsTab.tsx) was unlocked from
documentation/en/settings-tabs/01-flip-a-placeholder-tab-live.md's
"coming soon" state and rebuilt with real CRUD wired to the routes
above; its create/edit UI is a panel that expands inline in place
rather than a modal — see that file's WorkspaceSettingsLabelInlineEditor.tsx
if you're adding another editable-in-place settings row and want to
match the pattern instead of reaching for a Modal.
