Why Do GitLab Issue Board Labels Turn Into Workflow Chaos?
GitLab's most common issue board column is a label list: create a list, pick a label, and every issue carrying that label shows up as a card in it. On the Free tier, label lists (plus the built-in Open and Closed lists) are the only kind of column available, so for the majority of GitLab boards, the column is the label, full stop. Higher tiers can also build columns from assignees, milestones, iterations, or work item status instead of a label, but label lists remain the default, the free option, and the pattern most teams reach for first. The moment a team also uses labels for priority, product area, or "a customer asked for this," it's asking one flat namespace to carry board position, category, and provenance at the same time, and nothing stops one issue from ending up with in-progress, customer-request, and bug all fighting for meaning on the same card.
The fix that actually holds is to stop treating labels as one namespace, and split them into scoped groups: workflow::, priority::, type::. GitLab enforces one value per group, so workflow::doing and workflow::done can't both sit on a card. But scoped labels are a Premium and Ultimate feature, and even with them, the label that says "a customer asked for this" is still just a label. It tells you a request exists, not who asked, how many times, or whether it shipped. That gap is where a second system usually has to start.
Label lists are labels, and GitLab has said so itself
This isn't a guess about GitLab's internals. GitLab's own issue tracker has an open ticket from the early board days that names the exact problem: labels represent categories like "UX" or "Frontend" for organizing issues, and the same labels represent board columns like "To Do" and "Doing" for workflow. The ticket lists "solve the confusion of labels being overloaded as board stages" as an open concern, and floats a "Stage" entity as one possible fix, a sign that GitLab itself saw the collision as a design flaw, not user error.
The current docs confirm the mechanism plainly: on an issue board, "issues appear as cards in vertical lists, organized by their assigned labels, milestones, iterations, assignees, or status." For a label list specifically, that list is populated automatically from issues carrying the matching label, and dragging a card to a new list adds that list's label to the issue and removes the old one. There's no separate "label-list state" field behind the scenes: for that column type, the column is the label. Assignee, milestone, iteration, and status lists key off their own field instead of a label, which is why the collision this guide covers is a label-list problem specifically, not something every GitLab board hits, and why it's worth checking which list type your board actually uses before assuming the fix below applies.
Where the three purposes start fighting
Most teams back into the collision the same way, in roughly this order:
- Category labels come first.
frontend,backend,billing, the organizing labels nobody thinks twice about. - Board columns get bolted on. Someone builds a board and needs columns, so
to-do,doing,review,doneget created as labels, because that's the only way a board list works. - A provenance label shows up last. Support or sales wants visibility into what customers are asking for, so
customer-requestgets added to the same flat list.
Nothing in GitLab stops an issue from carrying frontend, doing, and customer-request at once, which is fine until someone builds a second board scoped to customer-request for a roadmap review and discovers the column labels from the engineering board are polluting it, or a triager removes doing to mean "not started yet" and inadvertently also strips the one label that said a paying customer had asked for this.
The fix: scoped labels enforce one value per axis
GitLab's answer to exactly this problem is scoped labels: a key::value syntax where GitLab won't let an issue hold two labels sharing the same key. Apply workflow::doing to an issue that already has workflow::review, and GitLab silently drops the old one. That's the mechanism that makes board columns behave like an actual state machine instead of an unmanaged tag pile. The same replacement happens when you drag a card between label lists on a board, so the column-as-label problem and the mutual-exclusion fix live in the same feature.
A workable split looks like:
workflow::todo,workflow::doing,workflow::review,workflow::done(the board columns, one value at a time)priority::p0,priority::p1,priority::p2(urgency, independent of column)type::bug,type::feature,type::customer-request(what kind of work this is, independent of both)
Two caveats worth knowing before you build around this. First, scoped labels are gated to Premium and Ultimate tiers on GitLab.com, self-managed, and Dedicated. A team on the Free tier can adopt the key::value naming convention, but GitLab won't enforce the mutual exclusion for them, so drift is still possible by hand. Second, GitLab has started building a way out of labels-as-workflow entirely for teams that can afford it. Newer work item Status fields give issues a native status (To do, In progress, Done, Won't do, Duplicate by default) that's separate from labels and explicitly pitched as a replacement for "using labels to manage work item lifecycles." It's Premium and Ultimate only, same as scoped labels, but it already backs boards directly as its own Status list column type, alongside label lists, so a status-based board is a real alternative for teams on those tiers, not a someday feature. Free-tier teams don't get that option either way, which is the practical reason label lists, and the collision problem that comes with them, stay the common case this guide is written for.
The collision, as it played out at Fieldstone Robotics
Fieldstone Robotics builds fleet-coordination software for warehouse robots. Its platform project had grown an issue board with four label lists (backlog, in-progress, in-review, shipped), built years before anyone scoped the labels, and Soo-ah Byun leads platform engineering there. Support had separately started adding a plain customer-ask label to issues that traced back to a specific account, so sales could point at a filtered board view during renewal calls.
It worked until a sprint cleanup ahead of a board demo. Soo-ah flagged it in the team's #platform-eng channel:
Somebody just bulk-removed
in-progressoff eleven cards during the label cleanup andcustomer-askwent with it on four of them, because whoever ran the cleanup filtered on "stale workflow labels" andcustomer-askmatched the same query pattern. Two of those four are the SSO rollout thing Meridian Ops has been asking about since August. If sales pulls up the customer-request board before I fix this, it looks like nobody asked for SSO at all.
The immediate fix was mechanical. Converting the four workflow labels to workflow::backlog through workflow::shipped meant the bulk cleanup script could no longer touch them by matching on loose label text, since scoped labels don't get swept by a "remove labels matching /progress/" style query the way plain labels do. That stopped the specific accident. It didn't answer Soo-ah's harder question, which was how many customer-requested issues had lost their customer-ask label the same way over the past year, quietly, with nobody noticing until a renewal call went badly.
What scoped labels still don't carry
Scoped labels solve the mechanical collision: one value per axis, enforced by GitLab, board-safe. They don't solve the problem underneath it, which is that "a customer asked for this" is not workflow metadata. It's provenance, and provenance needs to survive things labels don't survive well:
- Bulk operations. Any label-matching cleanup, migration, or bot can still touch
type::customer-requestthe same way it touched Fieldstone's plaincustomer-asklabel, just with fewer false-positive matches. The requester's name was never in the label to begin with, so there's nothing left to recover once it's gone. - Cross-project and cross-group requests. A GitLab issue lives in exactly one project (see our guide on whether a GitLab issue can span multiple projects), so a request that touches two teams' repos gets two separate
customer-requestlabels on two separate issues, with no shared record that they're the same ask from the same account. - Counting and dedupe. A label tells you an issue is customer-sourced. It doesn't tell you the same request showed up in three issues, two Slack threads, and a support ticket, or which of those five mentions came from the same paying account. Our guide on telling customer-sourced GitLab issues from internal ideas covers the conventions teams build by hand for this, and where each one runs out.
That's the point where teams move the "who asked, how many times, has it shipped" question out of the label taxonomy entirely and into a system built for it. That's the category Modem works in. It reads GitLab issues, comments, and the Slack and support threads around them, dedupes the same request across all of them into one counted topic, and tracks asked, promised, and shipped as actual states on the requester record instead of a label that a cleanup script can strip. GitLab issues stay GitLab issues: workflow, priority, and type keep living on scoped labels exactly as above. The GitLab integration sits alongside that, carrying the "who and how many" that labels were never built to hold. We build Modem, so weigh that against your own setup; below the point where a spreadsheet or a saved label filter can still track requesters by hand, the scoped-label split in this guide is genuinely enough.
Rename your labels before your next cleanup script runs
Pick three label axes to start: workflow, priority, and type. Rename the existing plain labels to key::value form if you're on Premium or Ultimate, and write the naming convention down somewhere the whole team can see it. Then audit one thing specifically. Search closed and merged issues for your current customer-provenance label and check whether any board cleanup, bulk edit, or automation has ever silently dropped it. If Fieldstone's story sounds plausible for your tracker, it already has.
