Back to list
jeremylongshore

linear-webhooks-events

by jeremylongshore

Hundreds of Claude Code plugins with embedded AI skills. Learn via interactive Jupyter tutorials.

1,042🍴 135📅 Jan 23, 2026

SKILL.md


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 TypeDescription
IssueIssue created, updated, or removed
IssueCommentComment added or updated
ProjectProject changes
CycleCycle (sprint) changes
LabelLabel changes
ReactionEmoji 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

ErrorCauseSolution
Invalid signatureWrong secret or tamperingVerify webhook secret
TimeoutProcessing too slowUse async queue
Duplicate eventsWebhook retryImplement idempotency
Missing dataPartial eventHandle gracefully

Resources

Next Steps

Optimize performance with linear-performance-tuning.

Score

Total Score

85/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 1000以上

+15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

+5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon