linear-webhooks-eventsClaude Skill
Configure and handle Linear webhooks for real-time event processing.
1.4k Stars
173 Forks
2025/10/10
| name | linear-webhooks-events |
| description | 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> |
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
// 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<string> { 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
// lib/webhook-handlers.ts interface LinearWebhookPayload { action: "create" | "update" | "remove"; type: string; data: Record<string, unknown>; createdAt: string; organizationId: string; webhookTimestamp: number; webhookId: string; } type EventHandler = (data: Record<string, unknown>, action: string) => Promise<void>; const handlers: Record<string, EventHandler> = { 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, action: string) { console.log(`Project ${action}:`, data); } async function handleCycleEvent(data: Record<string, unknown>, action: string) { console.log(`Cycle ${action}:`, data); }
Step 3: Business Logic Handlers
// 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
# 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
// 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
# 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
// lib/idempotency.ts import Redis from "ioredis"; const redis = new Redis(process.env.REDIS_URL); export async function processIdempotent( webhookId: string, processor: () => Promise<void> ): Promise<boolean> { 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
Next Steps
Optimize performance with linear-performance-tuning.
Similar Claude Skills & Agent Workflows
git-commit
5.4k
Generate well-formatted git commit messages following conventional commit standards
code-review
5.4k
Comprehensive code review assistant that analyzes code quality, security, and best practices
dsql
7.9k
Build with Aurora DSQL - manage schemas, execute queries, and handle migrations with DSQL-specific requirements.
backend-dev-guidelines
20.7k
Comprehensive backend development guide for Langfuse's Next.js 14/tRPC/Express/TypeScript monorepo.
Material Component Dev
7.6k
FlowGram 物料组件开发指南 - 用于在 form-materials 包中创建新的物料组件
Create Node
7.6k
用于在 FlowGram demo-free-layout 中创建新的自定义节点,支持简单节点(自动表单)和复杂节点(自定义 UI)