Linear API Expertise
Tool Preference Order
linearisCLI (recommended) - Simple, handles state name→UUID automatically- GraphQL API - For operations the CLI doesn't support
- Linear MCP - Avoid, currently flaky
CLI vs GraphQL Capabilities
| Operation | linearis CLI | GraphQL API |
|---|---|---|
| Read/search issues | ✅ | ✅ |
| Update issue state | ✅ | ✅ |
| Create issues | ✅ | ✅ |
| Add comments | ✅ | ✅ |
| List teams | ✅ | ✅ |
| Create teams | ❌ | ✅ |
| Create workflow states | ❌ | ✅ |
| Delete/archive states | ❌ | ✅ |
| Issue relations | ❌ | ✅ |
| Issue history | ❌ | ✅ |
| Create labels | ❌ | ✅ |
| Team setup scripts | ❌ | ✅ |
Use CLI for day-to-day operations. Use GraphQL for team/project setup and advanced queries.
When to Use This Skill
Load this skill when you need to:
- Create, read, update, or search Linear issues
- Transition issues through workflow states
- Add comments to issues
- Manage workflow states (create custom statuses)
- Query team information
Quick Reference - linearis CLI
For most common operations, use the CLI:
# Read issue
linearis issues read AF-96
# Update state
linearis issues update AF-96 --state "In Progress"
linearis issues update AF-96 --state "Waiting for Feedback"
# Add comment
linearis issues comment AF-96 "Progress update: completed implementation"
# Create issue - ALWAYS use team UUID, not abbreviation (abbreviations route to wrong team)
# Look up UUID: project-registry <project> linear.team_id
linearis issues create --team "79b48ab6-d677-4f3a-8228-86e30ce923a3" "[Feature] New capability" -d "Details..."
# Search
linearis issues search "team:AF state:\"In Progress\""
GraphQL API (for advanced queries)
Use GraphQL when the CLI doesn't support the operation (e.g., history, relations, team setup).
API Endpoint
https://api.linear.app/graphql
Authentication
# Via environment variable (recommended)
curl -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{"query": "..."}'
Get your API key: Linear Settings → Security & Access → API → Personal API keys
Issue ID Formats
- UUID:
2d40be5c-72ea-432e-a222-e169c7e8e21d(internal) - Identifier:
AF-96(human-readable, works in most queries)
Part 1: Reading Data
Get Single Issue
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "query GetIssue($id: String!) { issue(id: $id) { id identifier title description state { name } assignee { name } team { key name } labels { nodes { name } } comments { nodes { body createdAt user { name } } } } }",
"variables": { "id": "AF-96" }
}' | jq
Search Issues
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "query SearchIssues($query: String!, $first: Int) { searchIssues(query: $query, first: $first) { nodes { id identifier title state { name } } } }",
"variables": { "query": "team:AF state:\"In Progress\"", "first": 10 }
}' | jq
Search syntax:
team:AF- Filter by team keystate:"In Progress"- Filter by state nameassignee:@me- Assigned to current userlabel:bug- Has specific label
List Teams
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "{ teams { nodes { id key name } } }"
}' | jq
Get Workflow States for a Team
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "query GetStates($teamId: String!) { team(id: $teamId) { states { nodes { id name type position color } } } }",
"variables": { "teamId": "TEAM_UUID" }
}' | jq
State types: backlog, unstarted, started, completed, canceled
Part 2: Writing Data
Update Issue State
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation UpdateIssue($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success issue { id identifier state { name } } } }",
"variables": { "id": "AF-96", "stateId": "STATE_UUID" }
}' | jq
To find state UUID, query workflow states for the team first.
Create Issue
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation CreateIssue($title: String!, $teamId: String!, $description: String) { issueCreate(input: { title: $title, teamId: $teamId, description: $description }) { success issue { id identifier url } } }",
"variables": {
"title": "[Feature] - New capability description",
"teamId": "TEAM_UUID",
"description": "## Business Context\n\n## Acceptance Criteria\n- [ ] Criterion 1"
}
}' | jq
Add Comment to Issue
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation AddComment($issueId: String!, $body: String!) { commentCreate(input: { issueId: $issueId, body: $body }) { success comment { id body } } }",
"variables": {
"issueId": "AF-96",
"body": "Progress update:\n- Completed initial implementation\n- Tests passing"
}
}' | jq
Create Workflow State
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation CreateState($teamId: String!, $name: String!, $type: String!, $color: String!) { workflowStateCreate(input: { teamId: $teamId, name: $name, type: $type, color: $color }) { success workflowState { id name type color } } }",
"variables": {
"teamId": "TEAM_UUID",
"name": "Waiting for Feedback",
"type": "started",
"color": "#f59e0b"
}
}' | jq
State types:
backlog- Not started, low priorityunstarted- Not started, scheduledstarted- In progresscompleted- Done successfullycanceled- Won't do
Part 3: Helper Scripts
Get State ID by Name
# Get the UUID for a state by name (e.g., "In Progress")
get_state_id() {
local team_id="$1"
local state_name="$2"
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d "{
\"query\": \"query { team(id: \\\"$team_id\\\") { states { nodes { id name } } } }\"
}" | jq -r ".data.team.states.nodes[] | select(.name == \"$state_name\") | .id"
}
# Usage: get_state_id "TEAM_UUID" "In Progress"
Update Issue State by Name
# Combined: update issue to a named state
update_issue_state() {
local issue_id="$1"
local state_name="$2"
# First get the issue's team
local team_id=$(curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d "{\"query\": \"{ issue(id: \\\"$issue_id\\\") { team { id } } }\"}" \
| jq -r '.data.issue.team.id')
# Get the state ID
local state_id=$(get_state_id "$team_id" "$state_name")
# Update the issue
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d "{
\"query\": \"mutation { issueUpdate(id: \\\"$issue_id\\\", input: { stateId: \\\"$state_id\\\" }) { success } }\"
}" | jq
}
# Usage: update_issue_state "AF-96" "In Progress"
Part 4: AgentFlow Workflow Patterns
Linear States in AgentFlow
Discovered → Refining → Approved → In Progress → In Review → Dev → Test → Live
│ │ │ │ │ │ │ │
backlog started unstarted started started ←── completed ──→
State Transitions
| From | To | When |
|---|---|---|
| Discovered | Refining | Starting requirements analysis |
| Refining | Approved | Human approves mini-PRD |
| Approved | In Progress | Developer starts work |
| In Progress | In Review | PR created |
| In Review | Dev | PR merged to develop |
| Dev | Test | Deployed to test |
| Test | Live | Deployed to production |
Common Patterns
Starting work:
# Update to "In Progress"
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{"query": "mutation { issueUpdate(id: \"AF-96\", input: { stateId: \"STATE_UUID_FOR_IN_PROGRESS\" }) { success } }"}'
Waiting for feedback:
# Update to "Waiting for Feedback" (custom state)
# Add comment explaining what's needed
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{"query": "mutation { commentCreate(input: { issueId: \"AF-96\", body: \"Waiting for approval on approach\" }) { success } }"}'
PR created:
# Update to "In Review" and add PR link
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{"query": "mutation { issueUpdate(id: \"AF-96\", input: { stateId: \"STATE_UUID_FOR_IN_REVIEW\" }) { success } }"}'
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{"query": "mutation { commentCreate(input: { issueId: \"AF-96\", body: \"PR created: https://github.com/...\" }) { success } }"}'
Applying approval labels:
# Get label ID by name
LABEL_ID=$(curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{"query": "{ issueLabels(filter: { name: { eq: \"approval:bdd-pending\" } }) { nodes { id name } } }"}' \
| jq -r '.data.issueLabels.nodes[0].id')
# Apply label to issue (adds to existing labels)
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d "{\"query\": \"mutation { issueAddLabel(id: \\\"ISSUE_ID\\\", labelId: \\\"$LABEL_ID\\\") { success } }\"}"
# Remove label from issue
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d "{\"query\": \"mutation { issueRemoveLabel(id: \\\"ISSUE_ID\\\", labelId: \\\"$LABEL_ID\\\") { success } }\"}"
Part 5: Team Setup
AgentFlow Workflow State Definitions
AgentFlow uses a standardized set of workflow states. Colors are from the reference implementation (MIN project).
| State | Type | Color | Position | Purpose |
|---|---|---|---|---|
| Discovered | backlog | #bec2c8 | 0 | New feature identified, needs exploration |
| Refining | started | #5e6ad2 | 1 | Refinement phase in progress |
| Approved | unstarted | #bec2c8 | 2 | Refinement approved, ready for implementation |
| In Progress | started | #f2c94c | 3 | Active development |
| Waiting for Feedback | started | #eb5757 | 4 | Blocked on human input (agent automation) |
| In Review | started | #0f783c | 5 | PR created, code review |
| Dev | completed | #f2c94c | 6 | Merged to develop (feature branch only) |
| Test | completed | #26b5ce | 7 | Deployed to test environment (feature branch only) |
| Live | completed | #5e6ad2 | 8 | Released to production |
| Canceled | canceled | #95a2b3 | 9 | Won't do |
| Duplicate | canceled | #95a2b3 | 10 | Duplicate of another issue |
Development Model States
Trunk-based development (single environment):
- All states except Dev and Test
- Flow: ... → In Review → Live
Feature branch development (multiple environments):
- All states including Dev and Test
- Flow: ... → In Review → Dev → Test → Live
Create Team
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation CreateTeam($name: String!, $key: String!, $cyclesEnabled: Boolean) { teamCreate(input: { name: $name, key: $key, cyclesEnabled: $cyclesEnabled }) { success team { id key name } } }",
"variables": {
"name": "My Project",
"key": "MPR",
"cyclesEnabled": true
}
}' | jq
Key rules:
keymust be 2-5 uppercase letterskeymust be unique across the organizationcyclesEnabled: truerecommended for sprint planning
Delete Default Workflow State
After creating a team, Linear adds default states (Backlog, Todo, In Progress, Done, Canceled). Delete the ones that conflict with AgentFlow states.
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation DeleteState($stateId: String!) { workflowStateArchive(id: $stateId) { success } }",
"variables": { "stateId": "STATE_UUID_TO_DELETE" }
}' | jq
Note: You cannot delete a state if issues are assigned to it. Move issues first or use workflowStateArchive instead.
Create Label
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{
"query": "mutation CreateLabel($teamId: String!, $name: String!, $color: String) { issueLabelCreate(input: { teamId: $teamId, name: $name, color: $color }) { success issueLabel { id name } } }",
"variables": {
"teamId": "TEAM_UUID",
"name": "phase:discovery",
"color": "#5e6ad2"
}
}' | jq
AgentFlow Standard Labels
| Label | Color | Purpose |
|---|---|---|
| phase:setup | #95a2b3 | Setup phase work |
| phase:discovery | #5e6ad2 | Discovery phase work |
| phase:requirements | #f2c94c | Refinement phase work |
| phase:delivery | #0f783c | Delivery phase work |
| type:feature | #5e6ad2 | New functionality |
| type:bug | #eb5757 | Bug fix |
| type:improvement | #26b5ce | Enhancement to existing |
| type:documentation | #bec2c8 | Documentation work |
| approval:bdd-pending | #f59e0b | BDD scenarios pending review |
| approval:bdd-approved | #0f783c | BDD scenarios approved |
| approval:ux-pending | #f59e0b | UX design pending review |
| approval:ux-approved | #0f783c | UX design approved |
Complete Team Setup Script
#!/bin/bash
# Setup AgentFlow team in Linear
# Usage: ./setup-linear-team.sh "Project Name" "KEY" [trunk|feature]
PROJECT_NAME="$1"
TEAM_KEY="$2"
DEV_MODEL="${3:-feature}" # Default to feature branch
# 1. Create team
echo "Creating team: $PROJECT_NAME ($TEAM_KEY)..."
TEAM_RESULT=$(curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d "{
\"query\": \"mutation { teamCreate(input: { name: \\\"$PROJECT_NAME\\\", key: \\\"$TEAM_KEY\\\", cyclesEnabled: true }) { success team { id } } }\"
}")
TEAM_ID=$(echo "$TEAM_RESULT" | jq -r '.data.teamCreate.team.id')
if [ "$TEAM_ID" == "null" ]; then
echo "Error creating team: $(echo "$TEAM_RESULT" | jq -r '.errors[0].message')"
exit 1
fi
echo "Created team with ID: $TEAM_ID"
# 2. Get and archive default states
echo "Archiving default states..."
DEFAULT_STATES=$(curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d "{\"query\": \"{ team(id: \\\"$TEAM_ID\\\") { states { nodes { id name } } } }\"}" \
| jq -r '.data.team.states.nodes[] | "\(.id)|\(.name)"')
while IFS='|' read -r state_id state_name; do
echo " Archiving: $state_name"
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d "{\"query\": \"mutation { workflowStateArchive(id: \\\"$state_id\\\") { success } }\"}" > /dev/null
done <<< "$DEFAULT_STATES"
# 3. Create AgentFlow states
echo "Creating AgentFlow workflow states..."
create_state() {
local name="$1" type="$2" color="$3" position="$4"
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d "{
\"query\": \"mutation { workflowStateCreate(input: { teamId: \\\"$TEAM_ID\\\", name: \\\"$name\\\", type: \\\"$type\\\", color: \\\"$color\\\", position: $position }) { success } }\"
}" > /dev/null
echo " Created: $name"
}
create_state "Discovered" "backlog" "#bec2c8" 0
create_state "Refining" "started" "#5e6ad2" 1
create_state "Approved" "unstarted" "#bec2c8" 2
create_state "In Progress" "started" "#f2c94c" 3
create_state "Waiting for Feedback" "started" "#eb5757" 4
create_state "In Review" "started" "#0f783c" 5
if [ "$DEV_MODEL" == "feature" ]; then
create_state "Dev" "completed" "#f2c94c" 6
create_state "Test" "completed" "#26b5ce" 7
fi
create_state "Live" "completed" "#5e6ad2" 8
create_state "Canceled" "canceled" "#95a2b3" 9
create_state "Duplicate" "canceled" "#95a2b3" 10
# 4. Create labels
echo "Creating labels..."
create_label() {
local name="$1" color="$2"
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d "{
\"query\": \"mutation { issueLabelCreate(input: { teamId: \\\"$TEAM_ID\\\", name: \\\"$name\\\", color: \\\"$color\\\" }) { success } }\"
}" > /dev/null
echo " Created: $name"
}
create_label "phase:setup" "#95a2b3"
create_label "phase:discovery" "#5e6ad2"
create_label "phase:requirements" "#f2c94c"
create_label "phase:delivery" "#0f783c"
create_label "type:feature" "#5e6ad2"
create_label "type:bug" "#eb5757"
create_label "type:improvement" "#26b5ce"
create_label "type:documentation" "#bec2c8"
create_label "approval:bdd-pending" "#f59e0b"
create_label "approval:bdd-approved" "#0f783c"
create_label "approval:ux-pending" "#f59e0b"
create_label "approval:ux-approved" "#0f783c"
echo ""
echo "✅ Team setup complete!"
echo " Team: $PROJECT_NAME ($TEAM_KEY)"
echo " ID: $TEAM_ID"
echo " Model: $DEV_MODEL"
echo ""
echo "Add to project registry:"
echo " linear:"
echo " team_key: \"$TEAM_KEY\""
echo " team_id: \"$TEAM_ID\""
Best Practices
For AI Agents
- Cache team and state UUIDs - Query once at session start, reuse throughout
- Use issue identifiers (e.g.,
AF-96) not UUIDs for readability - Add comments for audit trail - Document decisions and progress
- Check response success - Always verify
success: truein mutations - Use search for discovery -
searchIssuesis powerful for finding related work
Error Handling
# Check for errors in response
response=$(curl -s -X POST https://api.linear.app/graphql ...)
if echo "$response" | jq -e '.errors' > /dev/null; then
echo "Error: $(echo "$response" | jq -r '.errors[0].message')"
exit 1
fi
Rate Limits
- Linear has rate limits (currently generous for most use cases)
- Batch operations when possible (
issueBatchUpdate) - Cache responses when data doesn't change frequently
Troubleshooting
"Entity not found"
- Check issue identifier is correct (case-sensitive)
- Ensure you have access to the team
"Invalid state transition"
- Some Linear workflows enforce state ordering
- Check team's workflow settings
"Unauthorized"
- Verify
LINEAR_API_KEYis set and valid - Check API key has required scopes
Essential Reading
Remember:
- Prefer
linearisCLI for common operations (read, update state, comment, create) - Use GraphQL API only for advanced operations (history, relations, team setup)
- Avoid Linear MCP - currently flaky
- Issue identifiers (e.g.,
AF-96) work in CLI and most GraphQL queries - Always add comments for traceability