The Context Switch That Kills Your Flow

You're debugging a production issue. You find the root cause — a missing null check in the order handler. You need to create a Linear issue before you forget the details.

So you open Linear. Click "New Issue." Pick the team. Type the title. Write the description. Set the priority. Add labels. Click "Create."

By the time you're done, 90 seconds have passed. You've lost the thread of what you were debugging. You switch back to the code. Where was that null check again?

What if you could just speak?

⌘4

"The order handler crashes on null shipping address. Missing null check in process_order at line 142. P2 bug, affects about 5 percent of international orders."

Release. SpeechButton transcribes your words, transforms them into a structured Linear issue, and sends it through the GraphQL API. A new issue appears in your backlog:

ENG-847: Order handler crashes on null shipping address

Missing null check in process_order at line 142. Affects ~5% of international orders where shipping address is optional.

Priority: High  |  Labels: bug, orders

Three seconds. No browser. No clicking. No context switch. You're still in your editor, still looking at the code.

How It Works

Your voice ──▶ SpeechButton STT ──▶ Gemma 4 Local AI ──▶ Linear GraphQL API
  (7ms)         (Parakeet V3,        (structures as         (creates issue)
                 100% offline)         title + desc +
                                       priority, local)

Three components:

  1. SpeechButton captures and transcribes your voice locally on Apple Neural Engine
  2. Local AI (Gemma 4) reads your prompt file and extracts a structured issue (title, description, priority) from your transcription — entirely on your Mac
  3. A Python script in your integrations/ folder sends the JSON to Linear's GraphQL API

The result: a well-formatted Linear issue from your spoken description, in under 4 seconds total. Everything is local except the final Linear API call — no Anthropic API key needed.

Setup

Five steps. Under five minutes.

Step 1: Get your Linear API key

  1. Open Linear → Settings → Security & Access → API
  2. Click "Create new API key"
  3. Copy the key (starts with lin_api_)
  4. Add to your shell profile: export LINEAR_API_KEY="lin_api_xxxxx"

Step 2: Choose your transform

SpeechButton supports two ways to structure your speech into a Linear issue. Option A is recommended — it's free, offline, and private.

Option A — Recommended Local AI · Free · Offline · Private

Uses Gemma 4 running locally on your Mac. No API key required. No data leaves your machine except the final Linear issue.

transform = "prompts/linear_issue.md"
Option B — Alternative Claude API · Requires API key · Costs money

Uses Claude API for potentially more accurate structuring. Requires an ANTHROPIC_API_KEY and incurs per-request costs.

transform = "transforms/transform_claude.py prompts/linear_issue.md"

The rest of this guide uses Option A (local AI). If you choose Option B, set ANTHROPIC_API_KEY in your environment and swap the transform line in config.toml. The integration script and prompt file are identical for both options.

Step 3: SpeechButton config.toml

Add a hotkey for Linear. RightCommand creates an issue; the default left Command still pastes at cursor.

toml — ~/.config/speechbutton/config.toml
# ~/.config/speechbutton/config.toml

[global]
model = "parakeet-tdt-0.6b-v3-int8"
language = "en"
auto_punctuation = true

[audio]
vad_enabled = true
vad_silence_threshold = 1.0

# Linear — create issue from voice
[[hotkey]]
key = "RightCommand"
channel = "4"
name = "linear"
transform = "prompts/linear_issue.md"
exec = "LINEAR_API_KEY=lin_api_xxx integrations/send_linear.py"

Step 4: Prompt file — spoken words → structured issue

Your raw speech is structured into a clean JSON payload for Linear. The transform field points to a prompt file that SpeechButton passes to the AI along with your transcription.

You say:

"The order handler crashes on null shipping address. Missing null check in process_order at line 142. P2 bug, affects about 5 percent of international orders."

markdown — ~/.config/speechbutton/prompts/linear_issue.md
You are a task formatter for Linear issue tracker.

Convert raw speech into a JSON object. Extract:
- title: concise issue title (under 80 chars)
- description: detailed markdown description
- priority: 1=urgent, 2=high, 3=medium, 4=low

Output ONLY valid JSON:
{"title": "...", "description": "...", "priority": 3}

What the transform produces:

json — structured issue
{
  "title": "Order handler crashes on null shipping address",
  "description": "Missing null check in `process_order` at line 142.\n\nAffects ~5% of international orders where the shipping address field is optional.\n\n**Impact:** Crashes on order processing for international customers.",
  "priority": 2
}

The AI infers priority from your words — "P2" becomes priority: 2, "urgent" becomes priority: 1, "minor" becomes priority: 4. Edit the prompt file any time to change the output format — no recompilation needed.

Step 5: Integration script — create the issue via Linear API

Takes the structured JSON from stdin and creates the issue via GraphQL. No external dependencies — uses only Python's standard library.

python — ~/.config/speechbutton/integrations/send_linear.py
#!/usr/bin/env python3
"""Create a Linear issue from stdin JSON."""
import json, os, sys, urllib.request

API_URL = "https://api.linear.app/graphql"

def graphql(key, query, variables=None):
    payload = {"query": query}
    if variables: payload["variables"] = variables
    req = urllib.request.Request(API_URL, json.dumps(payload).encode(),
        {"Content-Type": "application/json", "Authorization": key})
    return json.loads(urllib.request.urlopen(req, timeout=15).read()).get("data", {})

def main():
    text = sys.stdin.read().strip()
    if not text: sys.exit(0)

    # Strip markdown code fences
    if text.startswith("```"):
        lines = text.split("\n")
        text = "\n".join(lines[1:-1] if lines[-1].startswith("```") else lines[1:])

    try:
        data = json.loads(text)
    except json.JSONDecodeError:
        data = {"title": text[:80], "description": text, "priority": 3}

    key = os.environ.get("LINEAR_API_KEY")
    if not key:
        print("LINEAR_API_KEY not set", file=sys.stderr)
        sys.exit(1)

    # Auto-detect first team
    teams = graphql(key, "query { teams { nodes { id name } } }")
    team_id = teams["teams"]["nodes"][0]["id"]

    # Create issue
    mutation = """mutation($input: IssueCreateInput!) {
        issueCreate(input: $input) { success issue { identifier title url } }
    }"""
    result = graphql(key, mutation, {"input": {
        "teamId": team_id,
        "title": data.get("title", "Untitled"),
        "description": data.get("description", ""),
        "priority": data.get("priority", 3),
    }})
    issue = result["issueCreate"]["issue"]
    print(f"Created {issue['identifier']}: {issue['title'][:50]}")

if __name__ == "__main__":
    main()
bash — make executable
chmod +x ~/.config/speechbutton/integrations/send_linear.py

Done. Hold RightCommand, describe a bug, release. Linear issue created.

Real Workflows

During code review

You're reviewing a PR and spot three issues. Instead of writing inline comments that might get lost:

⌘4

"The database query in get_users is doing N+1. Should batch with a join or use dataloader. Medium priority, performance label."

⌘4

"The error handler on line 89 swallows the stack trace. Need to log the full error before returning the generic message. Low priority."

⌘4

"Race condition in the cache invalidation. Two requests can read stale data between the delete and the rebuild. High priority, needs fix before merge."

Three Linear issues created in 30 seconds. Each with the right priority, right description, right labels. You never left your code review.

During standup

Your team is discussing yesterday's incidents. As items come up, you capture them in real-time:

⌘4

"Follow up on the Redis timeout from yesterday. Connection pool might be exhausted under load. Need to add monitoring and possibly increase pool size. P3, assign to infra team."

No one has to remember to "create a ticket after standup." The ticket already exists.

During debugging

You're deep in a debugging session and keep finding related issues:

⌘4

"While investigating the auth bug I found that the rate limiter is using a fixed window instead of sliding window. This means users can burst double the limit at window boundaries. P3, technical debt."

You found it, you logged it, you moved on. Zero interruption to your debugging flow.

Advanced: Multiple Teams, Auto-Assign

For teams with multiple Linear teams (Engineering, Design, DevOps), set up a channel per team:

toml — multiple team hotkeys
# Engineering bugs
[[hotkey]]
key = "RightCommand"
channel = "4"
name = "linear-eng"
transform = "prompts/linear_issue.md"
exec = "LINEAR_API_KEY=lin_api_eng_xxx integrations/send_linear.py"

# DevOps issues
[[hotkey]]
key = "RightOption"
channel = "5"
name = "linear-devops"
transform = "prompts/linear_issue.md"
exec = "LINEAR_API_KEY=lin_api_devops_xxx integrations/send_linear.py"

Same prompt file, different API keys scoped to each team. RightCommand for engineering bugs, RightOption for infra issues. The right team gets the right issues without you navigating team selectors.

You can also extend the transform to auto-assign based on keywords. Mention "database" and it assigns to your DBA. Mention "auth" and it routes to the security lead. The transform script is just code — make it as smart as you need.

Why Voice Beats Typing for Issue Creation

Linear issues created by typing tend to be either too terse ("fix auth bug") or too slow to write (two minutes of formatting). Voice hits the sweet spot:

You speak naturally

"The order handler crashes on null shipping address, missing null check in process_order at line 142, P2, affects about 5 percent of international orders."

The transform structures it

Order handler crashes on null shipping address

Missing null check in process_order at line 142. Affects ~5% of international orders.

Priority: High  |  Labels: bug, orders

The result is a better issue than you'd type manually, created in a fraction of the time. The AI transform handles the formatting you'd skip when rushing and the structure you'd over-think when not.

And because SpeechButton captures in 7ms, you can rapid-fire three issues in sequence without pausing between them. Hold, speak, release. Hold, speak, release. Hold, speak, release. Three issues in your backlog before you'd finish typing the first one.

Privacy

Here's exactly what stays on your Mac and what goes to the cloud:

Component Where it runs Data sent externally
Voice capture Your Mac Nothing
Speech-to-text (Parakeet V3) Apple Neural Engine Nothing
AI transform (default) Your Mac (Gemma 4, local) Nothing
Linear API call Your Mac Structured issue → Linear's servers

With the default local AI transform, everything stays on your Mac except the final Linear API call: voice → local STT (Parakeet V3, Apple Neural Engine) → local AI transform (Gemma 4, on your Mac) → Linear API. No audio leaves your machine. No transcription leaves your machine. Only the structured issue data reaches Linear's servers — which is where it needs to go anyway. No Anthropic API key required. If you prefer higher accuracy at the cost of privacy, you can optionally switch to the Claude API transform (Option B) — in that case, transcribed text is sent to Anthropic's servers for structuring.

Get Started

  1. 1 Download SpeechButton — free 15 minutes/day, no account needed
  2. 2 Get your Linear API key — Settings → API → Create key
  3. 3 Copy the config, prompt file, and integration script from this article into ~/.config/speechbutton/
  4. 4 Make the integration script executable: chmod +x ~/.config/speechbutton/integrations/send_linear.py
  5. 5 Hold RightCommand, describe an issue, release. Your first voice-created Linear issue in under a minute.

Start creating issues by voice today

Free 15 min/day · No account needed · macOS 15+ · Apple Silicon

 Download for macOS — Free

Pro ($7.99/mo) removes the daily limit. Requires macOS 15+ and Apple Silicon.