Channels

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_SECRET and LINEAR_API_KEY set in your environment

Environment variables

bash
LINEAR_WEBHOOK_SECRET=your-linear-webhook-secret
LINEAR_API_KEY=lin_api_xxxxxxxxxxxx

Both variables must be set for the channel to activate. If either is missing, the gateway logs Linear as inactive at startup.

Webhook setup

text
# Linear webhook configuration
# Settings → API → Webhooks → New webhook
# Webhook URL: https://your-domain.com/channels/linear/webhook
# Select events:
#   ✓ Issues
#   ✓ Issue comments
#   ✓ Issue labels

Point 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.

💡For local development, use ngrok or a similar tunnel to expose your gateway to the internet. Linear requires a publicly reachable HTTPS endpoint.

Handled events

Event typeActions processedAgent receives
Issuecreate, updateTitle, description, URL, state, priority, labels
CommentcreateIssue title, comment body, URL
IssueLabelcreateLabel 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:

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

bash
# 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
MethodPathDescription
GET/linear-mappingsList all mappings for the current workspace
POST/linear-mappingsCreate a mapping (409 on duplicate label)
PUT/linear-mappings/:idUpdate label name and/or agent ID (partial)
DELETE/linear-mappings/:idDelete 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.

yaml
# 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

yaml
channels:
  linear:
    enabled: true
    webhookSecret: your-webhook-secret  # overrides LINEAR_WEBHOOK_SECRET
    apiKey: lin_api_xxxxxxxxxxxx        # overrides LINEAR_API_KEY

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

TablePurpose
linear_label_agent_mappingsMaps label names to agent IDs per workspace (unique constraint on workspace + label)
linear_webhook_eventsDeduplication 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.