Corsair
PluginsLinear

Linear Webhooks

All available Linear webhook events

Linear webhooks allow you to receive real-time notifications when issues, projects, or comments are created, updated, or deleted. The Corsair plugin provides a clean interface for handling these events.

New to Corsair? Learn about webhooks, hooks, and multi-tenancy.

Full Implementation: See the Linear plugin source code.

Setup

Configure webhook hooks when initializing the Linear plugin:

corsair.ts
import { linear } from "corsair/plugins";

export const corsair = createCorsair({
    plugins: [
        linear({
            webhookHooks: {
                issues: {
                    create: {
                        after: async (ctx, result) => {
                            console.log("Issue created:", result.data.title);
                        },
                    },
                },
            },
        }),
    ],
});

To receive webhooks from Linear:

  1. Go to your Linear workspace settings
  2. Navigate to Webhooks
  3. Create a new webhook with your Corsair webhook URL: https://your-app.com/webhooks/linear
  4. Select the events you want to receive
  5. (Optional) Set a webhook secret for signature verification

Available Webhooks

create

issues.create

Event Type: Issue Created

When it fires:

  • A new issue is created in any team
  • Issues are imported from external systems
  • Issues are created via API or automations

Payload Structure:

{
    action: "create",
    type: "Issue",
    data: { 
        id: "issue-id", 
        title: "Bug: Login form not working", 
        identifier: "ENG-123", 
        teamId: "team-id", 
        stateId: "state-id", 
        priority: 1, 
    }, 
    url: "https://linear.app/team/issue/ENG-123",
    organizationId: "org-id",
}

Example Usage:

linear({
    webhookHooks: {
        issues: {
            create: {
                after: async (ctx, result) => {
                    // Send notification for high-priority issues
                    if (result.data.priority <= 2) {
                        await inngest.send({
                            name: "linear/high-priority-issue",
                            data: {
                                tenantId: ctx.tenantId,
                                issue: result.data,
                            },
                        });
                    }
                },
            },
        },
    },
})

Key Fields:

FieldTypeDescription
data.idstringUnique issue ID
data.titlestringIssue title
data.identifierstringHuman-readable identifier (e.g., "ENG-123")
data.teamIdstringTeam the issue belongs to
data.stateIdstringCurrent workflow state
data.priority0 | 1 | 2 | 3 | 4Issue priority (0=None, 1=Urgent, 2=High, 3=Medium, 4=Low)

update

issues.update

Event Type: Issue Updated

When it fires:

  • Issue fields are modified (title, description, priority, etc.)
  • Issue state changes (e.g., moved to "In Progress")
  • Issue is assigned or reassigned
  • Labels, estimates, or other metadata are updated

Payload Structure:

{
    action: "update",
    type: "Issue",
    data: {
        id: "issue-id",
        title: "Updated title",
        stateId: "new-state-id", 
        // ... all current fields
    },
    updatedFrom: { 
        stateId: "old-state-id", 
        title: "Old title", 
    }, 
    url: "https://linear.app/team/issue/ENG-123",
    organizationId: "org-id",
}

Example Usage:

linear({
    webhookHooks: {
        issues: {
            update: {
                after: async (ctx, result) => {
                    // Notify when issue is completed
                    const stateChanged = result.updatedFrom?.stateId !== result.data.stateId;
                    
                    if (stateChanged) {
                        // Fetch current state to check if completed
                        const issue = await ctx.endpoints.issuesGet({
                            id: result.data.id,
                        });
                        
                        if (issue.state.type === "completed") {
                            await inngest.send({
                                name: "linear/issue-completed",
                                data: {
                                    tenantId: ctx.tenantId,
                                    issue: result.data,
                                },
                            });
                        }
                    }
                },
            },
        },
    },
})

Key Fields:

FieldTypeDescription
dataIssueCurrent state of the issue
updatedFromPartial<Issue>Previous values of changed fields
updatedFrom.stateIdstringPrevious workflow state

remove

issues.remove

Event Type: Issue Deleted

When it fires:

  • Issue is permanently deleted
  • Issue is archived (in some configurations)

Payload Structure:

{
    action: "remove",
    type: "Issue",
    data: {
        id: "issue-id", 
        title: "Deleted issue",
        identifier: "ENG-123",
        // ... final state before deletion
    },
    url: "https://linear.app/team/issue/ENG-123",
    organizationId: "org-id",
}

Example Usage:

linear({
    webhookHooks: {
        issues: {
            remove: {
                after: async (ctx, result) => {
                    // Clean up external references
                    await inngest.send({
                        name: "linear/issue-deleted",
                        data: {
                            tenantId: ctx.tenantId,
                            issueId: result.data.id,
                        },
                    });
                },
            },
        },
    },
})

Key Fields:

FieldTypeDescription
data.idstringID of deleted issue
data.identifierstringIssue identifier (e.g., "ENG-123")

Projects

create

projects.create

Event Type: Project Created

When it fires:

  • A new project is created
  • Projects are created via API or automations

Payload Structure:

{
    action: "create",
    type: "Project",
    data: {
        id: "project-id",
        name: "Q1 2024 Roadmap", 
        state: "started", 
        priority: 1,
    },
    url: "https://linear.app/project/project-name",
    organizationId: "org-id",
}

Example Usage:

linear({
    webhookHooks: {
        projects: {
            create: {
                after: async (ctx, result) => {
                    // Create tracking channel for new project
                    await inngest.send({
                        name: "linear/project-created",
                        data: {
                            tenantId: ctx.tenantId,
                            project: result.data,
                        },
                    });
                },
            },
        },
    },
})

Key Fields:

FieldTypeDescription
data.idstringUnique project ID
data.namestringProject name
data.statestringProject state (planned, started, paused, completed, canceled)

update

projects.update

Event Type: Project Updated

When it fires:

  • Project details are modified
  • Project state changes
  • Project dates are updated

Payload Structure:

{
    action: "update",
    type: "Project",
    data: {
        id: "project-id",
        name: "Q1 2024 Roadmap",
        state: "completed", 
        // ... all current fields
    },
    updatedFrom: { 
        state: "started", 
    }, 
    url: "https://linear.app/project/project-name",
    organizationId: "org-id",
}

Example Usage:

linear({
    webhookHooks: {
        projects: {
            update: {
                after: async (ctx, result) => {
                    // Notify when project is completed
                    if (result.data.state === "completed" && 
                        result.updatedFrom?.state !== "completed") {
                        await inngest.send({
                            name: "linear/project-completed",
                            data: {
                                tenantId: ctx.tenantId,
                                project: result.data,
                            },
                        });
                    }
                },
            },
        },
    },
})

Key Fields:

FieldTypeDescription
dataProjectCurrent project state
updatedFromPartial<Project>Previous values of changed fields

remove

projects.remove

Event Type: Project Deleted

When it fires:

  • Project is permanently deleted

Payload Structure:

{
    action: "remove",
    type: "Project",
    data: {
        id: "project-id", 
        name: "Deleted project",
        // ... final state before deletion
    },
    url: "https://linear.app/project/project-name",
    organizationId: "org-id",
}

Example Usage:

linear({
    webhookHooks: {
        projects: {
            remove: {
                after: async (ctx, result) => {
                    // Archive project data
                    await inngest.send({
                        name: "linear/project-deleted",
                        data: {
                            tenantId: ctx.tenantId,
                            projectId: result.data.id,
                        },
                    });
                },
            },
        },
    },
})

Comments

create

comments.create

Event Type: Comment Created

When it fires:

  • A new comment is added to an issue
  • Comments are created via API

Payload Structure:

{
    action: "create",
    type: "Comment",
    data: {
        id: "comment-id",
        body: "Great work on this! 👍", 
        issueId: "issue-id", 
        userId: "user-id",
    },
    url: "https://linear.app/team/issue/ENG-123#comment-id",
    organizationId: "org-id",
}

Example Usage:

linear({
    webhookHooks: {
        comments: {
            create: {
                after: async (ctx, result) => {
                    // Send notification for new comments
                    await inngest.send({
                        name: "linear/comment-created",
                        data: {
                            tenantId: ctx.tenantId,
                            comment: result.data,
                        },
                    });
                },
            },
        },
    },
})

Key Fields:

FieldTypeDescription
data.idstringUnique comment ID
data.bodystringComment content (markdown)
data.issueIdstringIssue the comment belongs to
data.userIdstringUser who created the comment

update

comments.update

Event Type: Comment Updated

When it fires:

  • Comment text is edited
  • Comment is modified

Payload Structure:

{
    action: "update",
    type: "Comment",
    data: {
        id: "comment-id",
        body: "Updated comment text", 
        editedAt: "2024-01-15T10:30:00Z", 
        issueId: "issue-id",
        userId: "user-id",
    },
    updatedFrom: { 
        body: "Original comment text", 
    }, 
    url: "https://linear.app/team/issue/ENG-123#comment-id",
    organizationId: "org-id",
}

Example Usage:

linear({
    webhookHooks: {
        comments: {
            update: {
                after: async (ctx, result) => {
                    // Track comment edits
                    console.log("Comment edited:", result.data.id);
                },
            },
        },
    },
})

Key Fields:

FieldTypeDescription
data.bodystringUpdated comment text
data.editedAtstringTimestamp of last edit
updatedFrom.bodystringPrevious comment text

remove

comments.remove

Event Type: Comment Deleted

When it fires:

  • Comment is permanently deleted

Payload Structure:

{
    action: "remove",
    type: "Comment",
    data: {
        id: "comment-id", 
        body: "Deleted comment",
        issueId: "issue-id",
        userId: "user-id",
    },
    url: "https://linear.app/team/issue/ENG-123",
    organizationId: "org-id",
}

Example Usage:

linear({
    webhookHooks: {
        comments: {
            remove: {
                after: async (ctx, result) => {
                    // Clean up comment references
                    console.log("Comment deleted:", result.data.id);
                },
            },
        },
    },
})

Best Practices

1. Use Event Systems

Don't call Corsair API methods directly in webhook hooks. Use an event system like Inngest:

// ❌ DON'T: Call corsair directly in webhook hook
linear({
    webhookHooks: {
        issues: {
            create: {
                after: async (ctx, result) => {
                    // This can cause circular calls
                    await corsair.slack.api.chat.postMessage({...});
                },
            },
        },
    },
})

// ✅ DO: Send to event system
linear({
    webhookHooks: {
        issues: {
            create: {
                after: async (ctx, result) => {
                    await inngest.send({
                        name: "linear/issue-created",
                        data: { tenantId: ctx.tenantId, issue: result.data },
                    });
                },
            },
        },
    },
})

2. Check updatedFrom for Updates

When handling update events, always check updatedFrom to see what actually changed:

linear({
    webhookHooks: {
        issues: {
            update: {
                after: async (ctx, result) => {
                    // Only act if priority changed
                    if (result.updatedFrom?.priority !== result.data.priority) {
                        // Handle priority change
                    }
                },
            },
        },
    },
})

3. Handle Errors Gracefully

Webhook failures shouldn't break your application:

linear({
    webhookHooks: {
        issues: {
            create: {
                after: async (ctx, result) => {
                    try {
                        await inngest.send({...});
                    } catch (error) {
                        console.error("Failed to process webhook:", error);
                        // Don't throw - webhook will succeed
                    }
                },
            },
        },
    },
})

4. Use Webhook Signature Verification

For production, always verify webhook signatures:

linear({
    // Set webhook secret from Linear settings
    webhookSecret: process.env.LINEAR_WEBHOOK_SECRET,
})

Common Patterns

Pattern 1: Issue State Tracking

Track when issues move through workflow states:

linear({
    webhookHooks: {
        issues: {
            update: {
                after: async (ctx, result) => {
                    if (result.updatedFrom?.stateId) {
                        await inngest.send({
                            name: "linear/issue-state-changed",
                            data: {
                                tenantId: ctx.tenantId,
                                issueId: result.data.id,
                                fromState: result.updatedFrom.stateId,
                                toState: result.data.stateId,
                            },
                        });
                    }
                },
            },
        },
    },
})

Pattern 2: Cross-Platform Notifications

Sync issue updates to Slack or other platforms:

linear({
    webhookHooks: {
        issues: {
            create: {
                after: async (ctx, result) => {
                    await inngest.send({
                        name: "linear/notify-slack",
                        data: {
                            tenantId: ctx.tenantId,
                            message: `New issue: ${result.data.title}`,
                            issueUrl: result.url,
                        },
                    });
                },
            },
        },
    },
})

Pattern 3: Automated Workflows

Trigger automations based on issue changes:

linear({
    webhookHooks: {
        issues: {
            update: {
                after: async (ctx, result) => {
                    // Auto-escalate stale high-priority issues
                    if (result.data.priority === 1) {
                        const issue = await ctx.endpoints.issuesGet({
                            id: result.data.id,
                        });
                        
                        const daysSinceUpdate = 
                            (Date.now() - new Date(issue.updatedAt).getTime()) / 
                            (1000 * 60 * 60 * 24);
                        
                        if (daysSinceUpdate > 2) {
                            await inngest.send({
                                name: "linear/escalate-issue",
                                data: {
                                    tenantId: ctx.tenantId,
                                    issue: result.data,
                                },
                            });
                        }
                    }
                },
            },
        },
    },
})