linear-webhooks-events

Configure and handle Linear webhooks for real-time event processing. Use when setting up webhooks, handling Linear events, or building real-time integrations. Trigger with phrases like "linear webhooks", "linear events", "linear real-time", "handle linear webhook", "linear webhook setup". allowed-tools: Read, Write, Edit, Bash(ngrok:*), Grep version: 1.0.0 license: MIT author: Jeremy Longshore <jeremy@intentsolutions.io>

Allowed Tools

No tools specified

Provided by Plugin

linear-pack

Claude Code skill pack for Linear (24 skills)

saas packs v1.0.0
View Plugin

Installation

This skill is included in the linear-pack plugin:

/plugin install linear-pack@claude-code-plugins-plus

Click to copy

Instructions

# Linear Webhooks & Events ## Overview Set up and handle Linear webhooks for real-time event notifications. ## Prerequisites - Linear workspace admin access - Public endpoint for webhook delivery - Webhook signing secret configured ## Available Event Types | Event Type | Description | |------------|-------------| | `Issue` | Issue created, updated, or removed | | `IssueComment` | Comment added or updated | | `Project` | Project changes | | `Cycle` | Cycle (sprint) changes | | `Label` | Label changes | | `Reaction` | Emoji reactions | ## Instructions ### Step 1: Create Webhook Endpoint ```typescript // api/webhooks/linear.ts (Vercel/Next.js style) import crypto from "crypto"; import type { NextApiRequest, NextApiResponse } from "next"; export const config = { api: { bodyParser: false, // Need raw body for signature }, }; async function getRawBody(req: NextApiRequest): Promise { const chunks: Buffer[] = []; for await (const chunk of req) { chunks.push(chunk); } return Buffer.concat(chunks).toString("utf8"); } function verifySignature(payload: string, signature: string): boolean { const secret = process.env.LINEAR_WEBHOOK_SECRET!; const hmac = crypto.createHmac("sha256", secret); const expectedSignature = hmac.update(payload).digest("hex"); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expectedSignature) ); } export default async function handler( req: NextApiRequest, res: NextApiResponse ) { if (req.method !== "POST") { return res.status(405).json({ error: "Method not allowed" }); } const rawBody = await getRawBody(req); const signature = req.headers["linear-signature"] as string; if (!signature || !verifySignature(rawBody, signature)) { return res.status(401).json({ error: "Invalid signature" }); } const event = JSON.parse(rawBody); // Process event await processLinearEvent(event); return res.status(200).json({ received: true }); } ``` ### Step 2: Event Processing Router ```typescript // lib/webhook-handlers.ts interface LinearWebhookPayload { action: "create" | "update" | "remove"; type: string; data: Record; createdAt: string; organizationId: string; webhookTimestamp: number; webhookId: string; } type EventHandler = (data: Record, action: string) => Promise; const handlers: Record = { Issue: handleIssueEvent, IssueComment: handleCommentEvent, Project: handleProjectEvent, Cycle: handleCycleEvent, }; export async function processLinearEvent(payload: LinearWebhookPayload) { const handler = handlers[payload.type]; if (!handler) { console.log(`No handler for event type: ${payload.type}`); return; } try { await handler(payload.data, payload.action); } catch (error) { console.error(`Error processing ${payload.type} event:`, error); throw error; } } async function handleIssueEvent(data: Record, action: string) { const issue = data as { id: string; identifier: string; title: string; state: { name: string }; priority: number; team: { key: string }; }; console.log(`Issue ${action}: ${issue.identifier} - ${issue.title}`); switch (action) { case "create": await onIssueCreated(issue); break; case "update": await onIssueUpdated(issue); break; case "remove": await onIssueRemoved(issue.id); break; } } async function handleCommentEvent(data: Record, action: string) { const comment = data as { id: string; body: string; issue: { identifier: string }; user: { name: string }; }; console.log(`Comment ${action} on ${comment.issue.identifier} by ${comment.user.name}`); } async function handleProjectEvent(data: Record, action: string) { console.log(`Project ${action}:`, data); } async function handleCycleEvent(data: Record, action: string) { console.log(`Cycle ${action}:`, data); } ``` ### Step 3: Business Logic Handlers ```typescript // lib/linear-handlers.ts import { sendSlackNotification } from "./slack"; import { syncToDatabase } from "./database"; async function onIssueCreated(issue: any) { // Sync to local database await syncToDatabase("issues", issue.id, issue); // Notify Slack for high-priority issues if (issue.priority <= 2) { await sendSlackNotification({ channel: "#engineering-alerts", text: `New high-priority issue: ${issue.identifier} - ${issue.title}`, }); } } async function onIssueUpdated(issue: any) { // Update local cache await syncToDatabase("issues", issue.id, issue); // Check for state changes if (issue.state?.name === "Done") { await celebrateCompletion(issue); } } async function onIssueRemoved(issueId: string) { await syncToDatabase("issues", issueId, null); // Soft delete } async function celebrateCompletion(issue: any) { console.log(`Issue completed: ${issue.identifier}`); } ``` ### Step 4: Register Webhook in Linear ```bash # Using Linear UI: # 1. Go to Settings > API > Webhooks # 2. Click "Create webhook" # 3. Enter your endpoint URL # 4. Select events to receive # 5. Save and copy the signing secret ``` ```typescript // Or via API import { LinearClient } from "@linear/sdk"; async function createWebhook() { const client = new LinearClient({ apiKey: process.env.LINEAR_API_KEY!, }); const result = await client.createWebhook({ url: "https://your-domain.com/api/webhooks/linear", label: "My Integration Webhook", teamId: "your-team-id", // Optional: limit to specific team resourceTypes: ["Issue", "IssueComment", "Project"], }); if (result.success) { const webhook = await result.webhook; console.log("Webhook created:", webhook?.id); console.log("Secret (save this!):", webhook?.secret); } } ``` ### Step 5: Local Development with ngrok ```bash # Start your local server npm run dev # Runs on localhost:3000 # In another terminal, start ngrok ngrok http 3000 # Copy the https URL and add to Linear webhook settings # Example: https://abc123.ngrok.io/api/webhooks/linear ``` ### Step 6: Idempotent Event Processing ```typescript // lib/idempotency.ts import Redis from "ioredis"; const redis = new Redis(process.env.REDIS_URL); export async function processIdempotent( webhookId: string, processor: () => Promise ): Promise { const key = `webhook:${webhookId}`; // Check if already processed const exists = await redis.exists(key); if (exists) { console.log(`Webhook ${webhookId} already processed, skipping`); return false; } // Mark as processing await redis.setex(key, 86400, "processing"); // 24 hour TTL try { await processor(); await redis.setex(key, 86400, "completed"); return true; } catch (error) { await redis.del(key); // Allow retry throw error; } } // Usage in webhook handler await processIdempotent(payload.webhookId, async () => { await processLinearEvent(payload); }); ``` ## Error Handling | Error | Cause | Solution | |-------|-------|----------| | `Invalid signature` | Wrong secret or tampering | Verify webhook secret | | `Timeout` | Processing too slow | Use async queue | | `Duplicate events` | Webhook retry | Implement idempotency | | `Missing data` | Partial event | Handle gracefully | ## Resources - [Linear Webhooks Documentation](https://developers.linear.app/docs/graphql/webhooks) - [Webhook Events Reference](https://developers.linear.app/docs/graphql/webhooks#webhook-events) - [ngrok Documentation](https://ngrok.com/docs) ## Next Steps Optimize performance with `linear-performance-tuning`.

Skill file: plugins/saas-packs/linear-pack/skills/linear-webhooks-events/SKILL.md