Linear
The Linear channel connects Open Astra agents to your Linear workspace. Agents receive issue, comment, and label events via webhooks — and post responses back as issue comments via the GraphQL API. Label-based routing lets you map specific labels to different agents, so adding agent:code-review to an issue automatically assigns the right agent.
Requirements
- A Linear workspace with API access enabled
- A webhook configured in Linear's settings (Settings → API → Webhooks)
- A Linear API key (personal or OAuth token) for posting comments back
LINEAR_WEBHOOK_SECRETandLINEAR_API_KEYset in your environment
Environment variables
LINEAR_WEBHOOK_SECRET=your-linear-webhook-secret
LINEAR_API_KEY=lin_api_xxxxxxxxxxxxBoth variables must be set for the channel to activate. If either is missing, the gateway logs Linear as inactive at startup.
Webhook setup
# Linear webhook configuration
# Settings → API → Webhooks → New webhook
# Webhook URL: https://your-domain.com/channels/linear/webhook
# Select events:
# ✓ Issues
# ✓ Issue comments
# ✓ Issue labelsPoint the webhook at /channels/linear/webhook on your Open Astra gateway. The gateway verifies every request using the Linear-Signature header (HMAC-SHA256, raw hex) with timingSafeEqual to prevent timing attacks. Invalid signatures receive a 401 response.
Handled events
| Event type | Actions processed | Agent receives |
|---|---|---|
Issue | create, update | Title, description, URL, state, priority, labels |
Comment | create | Issue title, comment body, URL |
IssueLabel | create | Label name, issue title, URL |
All other event types and actions receive a 200 response with { "status": "ignored" }. Issue descriptions and comment bodies are truncated at 2,000 characters.
How events reach the agent
Linear events are normalized into human-readable text before being passed to the agent loop. The actor's display name from the webhook payload is used as the sender identifier:
# Linear event → agent text format
# Issue create
"[Linear Issue Created] Fix login timeout
Description (up to 2000 chars)...
URL: https://linear.app/team/ISS-42
State: In Progress | Priority: Urgent
Labels: bug, agent:code-review"
# Issue update
"[Linear Issue Updated] Fix login timeout
Description (up to 2000 chars)...
URL: https://linear.app/team/ISS-42
State: Done | Priority: High
Labels: bug, agent:code-review"
# Comment create
"[Linear Comment on: Fix login timeout]
Comment body (up to 2000 chars)...
URL: https://linear.app/team/ISS-42"
# IssueLabel create
"[Linear Label Added: agent:code-review on: Fix login timeout]
URL: https://linear.app/team/ISS-42"Label-based agent routing
Unlike other channels that use a static defaultAgent, Linear routes issues to agents based on their labels. When a webhook arrives, the adapter extracts the issue's labels and looks up linear_label_agent_mappings for the first match. This lets different labels trigger different agents within the same workspace.
Manage label-to-agent mappings via the REST API at /linear-mappings (requires JWT auth):
# List all label → agent mappings
GET /linear-mappings
# Create a mapping
POST /linear-mappings
{ "labelName": "agent:code-review", "agentId": "code-review-agent" }
# Update a mapping
PUT /linear-mappings/:id
{ "agentId": "new-agent-id" }
# Delete a mapping
DELETE /linear-mappings/:id| Method | Path | Description |
|---|---|---|
GET | /linear-mappings | List all mappings for the current workspace |
POST | /linear-mappings | Create a mapping (409 on duplicate label) |
PUT | /linear-mappings/:id | Update label name and/or agent ID (partial) |
DELETE | /linear-mappings/:id | Delete a mapping |
Bidirectional responses
After the agent loop completes, the adapter posts the agent's response as a comment on the originating Linear issue via the GraphQL API. The issue ID serves as the conversation key throughout — from webhook ingestion to agent routing to comment posting.
Deduplication
Linear does not include a delivery ID header in webhooks. Instead, a synthetic delivery ID is constructed from the event payload: EventType:action:dataId:updatedAt. This ID is stored in the linear_webhook_events table. Duplicate deliveries — including across the webhook and heartbeat paths — are detected and skipped.
Heartbeat polling (optional)
In addition to webhooks, Linear can be polled on a schedule using the heartbeat system. This catches issues that were missed by the webhook path (e.g., during downtime) and processes them through the same agent loop.
# astra.yml — heartbeat-based polling (optional)
heartbeats:
- name: linear-issues
type: linear
interval: 5 # minutes between polls
config:
apiKey: lin_api_xxxxxxxxxxxx
labelPrefix: "agent:" # only fetch issues with this label prefix
teamId: TEAM_ID # optional — scope to one team
prompt: "Triage this Linear issue and suggest next steps."The heartbeat fetcher queries the Linear GraphQL API for issues updated since the last run, filters by label prefix, and cross-checks against linear_webhook_events to avoid reprocessing issues already handled by the webhook path. Each processed issue gets a heartbeat-prefixed delivery ID (heartbeat:issueId:updatedAt).
Configuration in astra.yml
channels:
linear:
enabled: true
webhookSecret: your-webhook-secret # overrides LINEAR_WEBHOOK_SECRET
apiKey: lin_api_xxxxxxxxxxxx # overrides LINEAR_API_KEYYAML config overrides environment variables. Setting enabled: false disables the channel even if env vars are present.
Database migration
Migration 035-linear-channel.sql creates two tables:
| Table | Purpose |
|---|---|
linear_label_agent_mappings | Maps label names to agent IDs per workspace (unique constraint on workspace + label) |
linear_webhook_events | Deduplication log shared between webhook and heartbeat paths |
Migrations run automatically on startup. No manual steps needed.
See also: Channels Overview for shared slash commands and how the channel adapter system works.