Linear Issue Creator
Bulk-creates Linear issues from story definitions using the Linear GraphQL API. Handles label taxonomy, W-number sequencing, and local file sync.
Prerequisites
LINEAR_API_KEYenvironment variable (or prompt user to paste alin_api_*key)- Team key (required — always ask, e.g.,
ARC,ENG) - Project name (optional — will match existing or note for user to create)
Step 0: Resolve Linear Context (Always Run First)
Before creating anything, query Linear to build a context map.
0a. Find team ID from key
{
teams {
nodes { id name key }
}
}
Match the user-provided team key (e.g., ARC) to get the teamId.
0b. Find project (if specified)
{
team(id: "<teamId>") {
projects {
nodes { id name }
}
}
}
Match by name. If not found, inform the user they need to create it in Linear first.
0c. Determine W-number high-water mark
{
issues(filter: { team: { key: { eq: "<TEAM_KEY>" } } }, first: 100, orderBy: createdAt) {
nodes { title }
}
}
Extract W-\d{6} from titles, find the maximum. If none exist, start at W-000001.
0d. Get existing labels
{
team(id: "<teamId>") {
labels {
nodes { id name }
}
}
}
Cache label IDs to avoid duplicates in Step 1.
Step 1: Label Taxonomy Setup
Create any missing labels from the standard taxonomy:
| Category | Labels |
|---|---|
| Epic | epic:<name> (project-specific, e.g., epic:foundation, epic:auth) |
| Priority | priority:P0, priority:P1, priority:P2 |
| Size | size:S, size:M, size:L, size:XL |
Create label mutation
mutation {
issueLabelCreate(input: {
name: "<label-name>",
teamId: "<teamId>"
}) {
success
issueLabel { id name }
}
}
Skip labels that already exist (matched by name from Step 0d).
Step 2: Story Body Format
Every issue follows arc-creating-user-stories/STORY_FORMAT.md:
Title format
[W-XXXXXX] <short descriptive title>
Body format
## User Story
**ID:** W-XXXXXX
When [situation], I want [motivation], so that [outcome].
## Acceptance Criteria
- [ ] [Observable outcome stated as a checkable fact]
- Verify: [test command, CLI check, or manual observation]
- [ ] [Another observable outcome]
- Verify: [how an implementer proves this criterion]
## Context
Technical context: affected files, patterns, dependencies.
**Type:** AFK | HITL
**Dependencies:** W-XXXXXX, W-XXXXXX
**Branch:** `feat/W-XXXXXX-short-name` | `fix/W-XXXXXX-short-name`
Required metadata per issue
| Field | Source | Required |
|---|---|---|
| Title | [W-number] description |
Yes |
| Description | Full story body above | Yes |
| Team ID | From Step 0a | Yes |
| Project ID | From Step 0b | If specified |
| Label IDs | From Step 1 (epic + priority + size) | Yes (3 labels minimum) |
Type definitions
- AFK (Away From Keyboard): Fully implementable by an AI agent without human input
- HITL (Human In The Loop): Requires human action (API keys, account setup, manual config)
Step 3: Bulk Issue Creation
Create issue mutation
mutation CreateIssue($input: IssueCreateInput!) {
issueCreate(input: $input) {
success
issue {
id
identifier
url
}
}
}
Variables
{
"input": {
"title": "[W-000001] Repository scaffold and CI/CD",
"description": "<full story body>",
"teamId": "<teamId>",
"projectId": "<projectId>",
"labelIds": ["<epicLabelId>", "<priorityLabelId>", "<sizeLabelId>"]
}
}
Execution rules
- Create issues sequentially (not parallel) to preserve ordering
- Rate limiting: pause 500ms every 5 issues to avoid API throttling
- Error handling: log failures but continue creating remaining issues
- Track results: collect
{W-number, identifier, url}for every created issue - Summary: print a table of all created issues at the end
Implementation pattern (Python)
import json, urllib.request, time
API_KEY = "<LINEAR_API_KEY>"
ENDPOINT = "https://api.linear.app/graphql"
def gql(query, variables=None):
payload = {"query": query}
if variables:
payload["variables"] = variables
req = urllib.request.Request(
ENDPOINT,
data=json.dumps(payload).encode(),
headers={
"Content-Type": "application/json",
"Authorization": API_KEY,
},
)
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())
# Create issues in a loop
for i, issue in enumerate(issues):
result = gql(CREATE_MUTATION, {"input": { ... }})
# Track result
if (i + 1) % 5 == 0:
time.sleep(0.5)
Alternative: curl (for small batches)
curl -s -X POST https://api.linear.app/graphql \
-H "Content-Type: application/json" \
-H "Authorization: $LINEAR_API_KEY" \
-d '{"query": "mutation { issueCreate(input: { title: \"...\", description: \"...\", teamId: \"...\", labelIds: [\"...\"] }) { success issue { identifier url } } }"}'
Step 4: Local File Sync
After all issues are created, update local tracking files.
progress.txt format
## Phase Name
[ ] W-000001 (ARC-11) Short title
[ ] W-000002 (ARC-12) Short title [HITL]
[x] W-000003 (ARC-13) Completed title
IMPLEMENTATION_PLAN.md format
Add Linear cross-references to each phase:
## Phase 1: Foundation (ARC-11 through ARC-16)
- W-000001 (ARC-11): Description of the story.
- W-000002 (ARC-12): Description of the story.
Cross-Skill Delegation
| Requirement | Delegate to | Why |
|---|---|---|
| Story format & acceptance criteria | arc-creating-user-stories | Canonical story body format |
| Vertical slice breakdown | arc-prd-to-issues | Tracer-bullet slice rules |
| Commit conventions & branch naming | arc-conventional-commits | feat/W-XXXXXX-* branch pattern |
| Plan pressure-testing | grill-me (global skill, not in this repo) |
Decision-complete plan before issue creation |
| Ongoing issue management | Linear MCP server | Triage, sprint planning, retrospectives |
Verification Checklist
After creating all issues, verify:
- All issues exist in Linear with correct project assignment
- Each issue has exactly 3 labels (epic + priority + size)
- No duplicate W-numbers in the team
- W-numbers are sequential from the high-water mark
- Dependency references in issue bodies are valid (W-numbers exist)
-
progress.txtupdated with W-number -> Linear ID mapping -
IMPLEMENTATION_PLAN.mdupdated with Linear cross-references - HITL issues are clearly marked in both Linear and local files
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
401 Unauthorized |
Invalid or expired API key | Re-generate at Linear Settings > API > Personal API Keys |
Team not found |
Wrong team key | Run teams query to list available keys |
Label already exists |
Duplicate creation attempt | Query existing labels first (Step 0d), skip matches |
| Rate limit errors | Too many requests | Increase pause interval between batches |
| Missing project | Project doesn't exist yet | User must create it in Linear dashboard first |
| W-number collision | Concurrent issue creation | Re-query high-water mark before each batch |
Authentication
The Linear GraphQL API uses a personal API key for authentication:
Authorization: lin_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Keys are created at: Linear > Settings > Account > API > Personal API Keys
The key must have write access to the target team. Store as LINEAR_API_KEY environment variable or prompt the user to paste it at runtime.
Security: Never commit API keys to git. If pasted in session, write to a temp file and delete after use. Do not store in SKILL.md or any tracked file.