linear-bug-workflow-runner
Author and run Linear bug workflows via the GraphQL API: issue creation, state transitions (workflowState assignment), priority assignment (0 No priority / 1 Urgent / 2 High / 3 Medium / 4 Low), label-based classification, search by team and content. Covers the issueCreate mutation, issueUpdate for state transitions, the workflowStates query for per-team state IDs, and Linear's API-key vs OAuth Bearer auth modes; resolve-by-type, CI wiring, and result parsing live in references/. Use when the target tracker is Linear specifically; for other trackers use jira-bug-workflow-runner (Jira) or github-issues-bug-workflow (GitHub Issues). Files and transitions the issue; reproducing the defect is a separate concern.
Install with skills.sh (any agent)
npx skills add testland/qa --skill linear-bug-workflow-runnerlinear-bug-workflow-runner
Overview
Linear's API is GraphQL-only. Unlike Jira's REST workflow engine, Linear's lifecycle is driven by WorkflowState objects: each team has its own states (Backlog / Todo / In Progress / In Review / Done / Cancelled, plus team-specific additions). To transition a defect, set its stateId to the target state's ID.
This skill wraps Linear's GraphQL API (linear.app/developers/graphql (opens in new window)) for create / update / transition / search.
When to use
How to use
Authentication
Per Linear API docs, two auth modes:
# Personal API key (lin_api_*)
export LINEAR_KEY="lin_api_xxxxxxxxxxxxxxxxxx"
# Or OAuth bearer token
export LINEAR_TOKEN="<oauth-access-token>"HEADERS_KEY = {
"Authorization": os.environ["LINEAR_KEY"], # personal key, no Bearer
"Content-Type": "application/json",
}
HEADERS_OAUTH = {
"Authorization": f"Bearer {os.environ['LINEAR_TOKEN']}",
"Content-Type": "application/json",
}
ENDPOINT = "https://api.linear.app/graphql"Note: personal API keys use the Authorization header without the Bearer prefix; OAuth tokens use Bearer. This is unusual - many GraphQL APIs reject the bareword auth - confirmed in Linear's quickstart.
Create a bug
The issueCreate mutation per linear.app/developers/graphql (opens in new window):
import requests, os
QUERY = """
mutation IssueCreate($input: IssueCreateInput!) {
issueCreate(input: $input) {
success
issue { id identifier url state { name } }
}
}
"""
def create_bug(team_id, title, description_md, priority, state_id, label_ids=None):
variables = {
"input": {
"teamId": team_id,
"title": title,
"description": description_md, # Markdown supported
"priority": priority, # 0=No, 1=Urgent, 2=High, 3=Med, 4=Low
"stateId": state_id, # initial state (e.g., "Backlog" or "Todo")
"labelIds": label_ids or [],
}
}
r = requests.post(ENDPOINT, json={"query": QUERY, "variables": variables},
headers=HEADERS_KEY)
r.raise_for_status()
data = r.json()
if data.get("errors"):
raise RuntimeError(data["errors"])
return data["data"]["issueCreate"]["issue"]Priority integer values
Per Linear's published priority enum (visible across the GraphQL schema and the dashboard tooltip):
| Integer | Label |
|---|---|
| 0 | No priority |
| 1 | Urgent |
| 2 | High |
| 3 | Medium |
| 4 | Low |
Reverse of what some might expect: 1 is highest urgency.
Discover state IDs per team
State IDs are per-team. Look them up via workflowStates query:
STATES_QUERY = """
query Workflow($teamId: String!) {
workflowStates(filter: { team: { id: { eq: $teamId } } }) {
nodes { id name type }
}
}
"""
def get_states(team_id):
r = requests.post(ENDPOINT,
json={"query": STATES_QUERY, "variables": {"teamId": team_id}},
headers=HEADERS_KEY)
r.raise_for_status()
return r.json()["data"]["workflowStates"]["nodes"]type is one of backlog, unstarted, started, completed, canceled - the canonical lifecycle bucket independent of the state's display name. Resolve by type, never by team-customisable name. (The unfiltered all-teams form of this query is in references/linear-graphql-reference.md.)
Transition (update state)
issueUpdate mutation:
UPDATE_QUERY = """
mutation IssueUpdate($id: String!, $input: IssueUpdateInput!) {
issueUpdate(id: $id, input: $input) {
success
issue { id state { name } }
}
}
"""
def transition(issue_id, new_state_id):
variables = {"id": issue_id, "input": {"stateId": new_state_id}}
r = requests.post(ENDPOINT, json={"query": UPDATE_QUERY, "variables": variables},
headers=HEADERS_KEY)
r.raise_for_status()
data = r.json()
return data["data"]["issueUpdate"]["success"]issueUpdate accepts the same input fields as issueCreate (except teamId which is immutable) plus assignee, due date, estimate, etc.
Search
The issues query supports filter expressions:
SEARCH_QUERY = """
query SearchIssues($filter: IssueFilter!) {
issues(filter: $filter, first: 50) {
nodes { id identifier title state { name } priority }
}
}
"""
def find_dupes(team_id, title_text):
r = requests.post(ENDPOINT,
json={"query": SEARCH_QUERY, "variables": {"filter": {
"team": {"id": {"eq": team_id}},
"title": {"contains": title_text},
"state": {"type": {"neq": "completed"}},
}}},
headers=HEADERS_KEY)
r.raise_for_status()
return r.json()["data"]["issues"]["nodes"]Filter operators: eq, neq, contains, startsWith, endsWith, plus comparison for numerics.
Worked example
File a bug from a CI failure idempotently: dedupe by title, comment on the existing issue if found, otherwise create it in the team's unstarted state. This reuses find_dupes, get_states, and create_bug above:
def create_or_attach(team_id, title, description):
dupes = find_dupes(team_id, title)
if dupes:
# Comment on the existing issue rather than duplicate
add_comment(dupes[0]["id"], f"Recurred: {description[:500]}")
return dupes[0]["identifier"]
todo_state = next(s for s in get_states(team_id) if s["type"] == "unstarted")
return create_bug(team_id, title, description, priority=3,
state_id=todo_state["id"])["identifier"]
# From a failing pytest run:
ident = create_or_attach(
team_id, "Payment webhook retries drop events",
"Repro: send 3 webhooks in 1s; 2 are lost.",
)
print(f"Bug tracked as {ident}") # e.g. ENG-1234add_comment uses the commentCreate mutation (similar shape; omitted for brevity). issueCreate.issue.identifier is the human-readable ID (e.g., ENG-1234); issueCreate.issue.url is the canonical permalink. To resolve the bug to Done after the fix ships without hard-coding state names, use the transition_to_completed helper in references/linear-graphql-reference.md.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Authorization: Bearer <lin_api_*> | Wrong header format for personal keys | Personal keys: header without Bearer; OAuth: header with Bearer |
| Hard-coded state names ("Done") | Team-renamed states break the runner | Resolve by type (canonical) not name (display) |
| Priority 1 = "low" | Reversed expectation; 1 = Urgent in Linear | Document the enum; use the constant table |
Plain-text in description field | Markdown is accepted but Linear renders blocks differently than Jira | Test rendering for code blocks / tables |
| Single workflowStates query for all teams | High latency on large workspaces; data overflow | Filter by team |
| No dedupe before create | Same flaky test generates many issues | Search by title contains; comment-attach |
Querying state { name } instead of state { type } | Filter logic breaks when team renames states | Query type for stability |
Limitations
References
Linear GraphQL deep reference
View source (opens in new window)Linear GraphQL deep reference
Deep reference for linear-bug-workflow-runner SKILL.md. Consult when resolving a workflow state by its lifecycle type without hard-coding names, discovering states across all teams, wiring CI to file bugs on failure, or parsing the identifiers returned by issueCreate.
Resolve workflow-state by type
Many automation flows want "transition to whatever the team uses as Done" without hard-coding state names. Resolve the target state by its lifecycle type, then reuse the transition helper from the SKILL:
def transition_to_completed(issue_id, team_id):
done = next(s for s in get_states(team_id) if s["type"] == "completed")
return transition(issue_id, done["id"])The type enum (backlog, unstarted, started, completed, canceled) is stable; the name is team-customisable, so resolving by type survives a team renaming its columns.
Discover states across all teams
Per the Linear quickstart docs the simpler, unfiltered form is:
query { workflowStates { nodes { id name } } }...which returns all states across all teams. Prefer the per-team filtered query in the SKILL - the unfiltered form has high latency on large workspaces and returns far more nodes than a single flow needs.
Parsing results
issueCreate.issue.identifier is the human-readable ID (e.g., ENG-1234). issueCreate.issue.url is the canonical permalink. GraphQL errors surface under a top-level errors array even on an HTTP 200 - check data.get("errors") before reading data["data"].
CI integration
- name: File Linear bug on failure
if: failure()
env:
LINEAR_KEY: ${{ secrets.LINEAR_KEY }}
LINEAR_TEAM_ID: ${{ vars.LINEAR_TEAM_ID }}
run: python scripts/file-linear-bug.py results.xmlfile-linear-bug.py reads the JUnit XML, extracts the first failure, deduplicates via find_dupes, and calls create_or_attach.
Related skills
azuredevops-bug-workflow
Authors and triages Bug work items in Azure DevOps Boards via the Work Item Tracking REST API (api-version 7.1) - Bug creation with JSON Patch, state transitions across New/Active/Resolved/Closed, and WIQL queries for triage queues and duplicate detection. Deep operational blocks (field-value fetch, PR / build artifact links, bulk close, az boards CLI, CI wiring) live in references/. Use when programmatically managing Azure DevOps Bug lifecycle states: creating from CI failures, triaging open defect queues, transitioning states in bulk, or attaching traceability links to builds and pull requests.
bug-lifecycle-reference
Pure-reference catalog of defect lifecycle states and transitions. Defines the ISTQB-canonical states (new / open / assigned / in-progress / fixed / verified / closed / reopened / deferred / rejected / duplicate) and the transitions between them, distinguishes the ISTQB terms (error → fault / defect → failure), maps the lifecycle to the standard Jira / Linear / GitHub Issues workflows, and cites IEEE 1044-2009 and ISO/IEC/IEEE 29119-3 for the canonical anchors. Use as the lifecycle vocabulary for bug-report review, duplicate detection, and the platform-workflow skills.
bug-report-from-failure
On-demand builder that converts a SINGLE test failure record (JUnit XML, Allure JSON, pytest --tb=short, Playwright HTML, Cypress mocha-junit) into a structured, tracker-agnostic bug SPEC: extracts test name, assertion, stack trace, environment, and artefacts, and proposes severity, defect type (IEEE 1044), and a root-cause hypothesis (ISTQB CTAL-TA), then hands the JSON spec to a jira/linear/github-issues-bug-workflow runner to file. Use when you already hold a failure artefact and want one classified, ready-to-file report. Distinct from the event-driven CI orchestrator that triggers automatically on a pipeline failure and files in bulk, and from screen-recording-driven bug reporting; this is the on-demand, single-record spec builder.
confirmation-testing-workflow
Procedure for proving that a claimed defect fix actually reached the build under test and actually works. Covers the merge-base ancestry check that proves the running build contains the fix commit rather than trusting a version label, the priority order for choosing which reproduction to re-run, and the VERIFIED / NOT FIXED / BLOCKED verdict table whose governing rule is that any ambiguous, flaky, or unreproducible result resolves to BLOCKED and is never guessed. Scoped to ISTQB confirmation testing (does this specific fix work?), not regression testing (did the fix break something else?), and not triage or severity assignment. Use when a developer has marked a defect Fixed and someone must decide whether it moves to Verified or back to Reopened.
defect-taxonomy-istqb
Pure-reference catalog of defect categorisation taxonomies. Covers the IEEE 1044-2009 anomaly classification (anomaly class, anomaly type, anomaly severity, root cause category), the ISTQB CTAL-TA root-cause taxonomy (requirements / design / implementation / interface / test-data / build-environment), and the Orthogonal Defect Classification (ODC) eight-attribute framework. Maps each taxonomy to a worked example showing how the same defect classifies under each. Use to categorise defects consistently across a team, drive root-cause analysis, and inform process-improvement decisions (where in the SDLC do we leak the most defects?).
github-issues-bug-workflow
Author and run GitHub Issues bug workflows via REST API (2026-03-10): issue creation, state changes (open / closed with `state_reason`), label-based severity/priority classification, and comment attachment. Covers `POST /repos/{owner}/{repo}/issues`, `PATCH` for `state_reason` transitions (completed / not_planned / duplicate / reopened), and label conventions for GitHub's binary open/closed model; Projects v2, `gh` CLI, and CI wiring live in references/. Use when programmatically managing the GitHub Issues bug lifecycle; for the same workflow on another tracker use azuredevops-bug-workflow, jira-bug-workflow-runner, or linear-bug-workflow-runner.
jira-bug-workflow-runner
Jira Cloud bug workflow runner using the REST API v3: issue creation with an ADF description, runtime transition lookup and apply, JQL search for triage queues and duplicate detection, severity/priority field updates, label-based classification (severity/priority/regression), and idempotent CI-driven filing from JUnit XML test failures. Use when the target tracker is Jira Cloud and the task involves Jira lifecycle states (create, triage, transition, close). Distinct from a platform-agnostic event-driven CI defect filer, and from linear-bug-workflow-runner / github-issues-bug-workflow for other trackers.
severity-vs-priority-reference
Pure-reference catalog distinguishing defect severity (impact on the system / user) from defect priority (urgency of fix), each on its own axis. Enumerates the canonical 5-point severity scale (Critical / High / Medium / Low / Trivial) and the 5-point priority scale (Immediate / High / Medium / Low / Deferred), explains why they must be tracked separately (a Critical/Deferred legacy bug exists; a Trivial/Immediate spelling bug on the homepage exists), maps to IEEE 1044-2009 severity classes, and ties to bug-lifecycle-reference state transitions. Use when triaging a defect, configuring a tracker's severity/priority fields, or reviewing whether a bug report assigned them consistently.