CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/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.

76

Quality

95%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

SKILL.md

name:
linear-bug-workflow-runner
description:
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.

linear-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) for create / update / transition / search.

When to use

  • Filing a bug from a CI test failure (consumed by bug-report-from-failure).
  • Transitioning bugs in bulk after a release.
  • Backing duplicate-defect search for Linear-using teams.

How to use

  1. Authenticate: personal API key (no Bearer) or OAuth token (with Bearer) - the header format differs (see Authentication).
  2. Look up the target team's workflowStates and resolve states by type, not display name (see Discover state IDs).
  3. Dedupe with the issues filter query, then issueCreate in an unstarted state; issueUpdate to transition as the fix progresses (see Search / Create / Transition).
  4. Resolve-by-type helpers, CI wiring, and result parsing are in references/linear-graphql-reference.md.

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:

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

IntegerLabel
0No priority
1Urgent
2High
3Medium
4Low

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-1234

add_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-patternWhy it failsFix
Authorization: Bearer <lin_api_*>Wrong header format for personal keysPersonal keys: header without Bearer; OAuth: header with Bearer
Hard-coded state names ("Done")Team-renamed states break the runnerResolve by type (canonical) not name (display)
Priority 1 = "low"Reversed expectation; 1 = Urgent in LinearDocument the enum; use the constant table
Plain-text in description fieldMarkdown is accepted but Linear renders blocks differently than JiraTest rendering for code blocks / tables
Single workflowStates query for all teamsHigh latency on large workspaces; data overflowFilter by team
No dedupe before createSame flaky test generates many issuesSearch by title contains; comment-attach
Querying state { name } instead of state { type }Filter logic breaks when team renames statesQuery type for stability

Limitations

  • GraphQL learning curve. Engineers used to REST may write noisy queries that over-fetch.
  • State type enum is small. Five values cover the lifecycle; fine-grained sub-states (e.g., "Code Review" vs "In Progress") share type: started. Use name + type together when needed.
  • Webhook complement. For event-driven workflows (notification on state change), pair with Linear webhooks rather than polling.
  • Personal API key bypasses 2FA - use OAuth bearer for user-impersonating flows.
  • Rate limits. ~1500 requests / 15 min per token; bulk operations need throttling.

References

  • Linear GraphQL API quickstart - linear.app/developers/graphql.
  • Linear API overview - linear.app/developers.
  • Linear GraphQL schema (Apollo Studio) - referenced from the developer docs.
  • Deep reference (resolve-by-type helper, all-teams query, CI wiring, result parsing): references/linear-graphql-reference.md.
  • Sibling references: bug-lifecycle-reference, severity-vs-priority-reference.
  • Sibling skills: jira-bug-workflow-runner, github-issues-bug-workflow.
  • Consumed by: bug-report-from-failure.

SKILL.md

tile.json