> ## Documentation Index
> Fetch the complete documentation index at: https://corsair-feat-reconnect-error.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Hatchet

> Push Hatchet workflow events from Corsair webhook hooks.

<Note>
  **Corsair Workflows (beta)**. Durable workflows, built in. No external engine to wire. [See how](/workflows/overview)
</Note>

Use Corsair's `webhookHooks` to push events into Hatchet whenever something happens in a connected service. Hatchet handles durable execution and retries. Corsair handles webhook routing and integration auth.

## Install

```bash theme={null}
npm install @hatchet-dev/typescript-sdk
```

## Event trigger

When a message arrives in Slack's #support channel, push a Hatchet event. The workflow creates a Linear issue to track the request and replies in-thread to confirm it was received.

```ts corsair.ts theme={null}
import { hatchet } from '@/hatchet/client';

slack({
    webhookHooks: {
        messages: {
            message: {
                before: async (ctx, payload) => {
                    // Only handle messages in the #support channel
                    if (payload.channel !== process.env.SLACK_SUPPORT_CHANNEL) {
                        throw new Error('Not the support channel, skipping');
                    }
                    // Skip bot messages
                    if (payload.bot_id) throw new Error('Bot message, skipping');
                    return { ctx, payload };
                },
                after: async (ctx, result) => {
                    await hatchet.client.event.push('slack:support.message', {
                        channel: result.data.channel,
                        threadTs: result.data.ts,
                        text: result.data.text,
                        userId: result.data.user,
                        tenantId: ctx.tenantId,
                    });
                },
            },
        },
    },
})
```

```ts hatchet/client.ts theme={null}
import Hatchet from '@hatchet-dev/typescript-sdk';

export const hatchet = await Hatchet.init();
```

```ts hatchet/workflows.ts theme={null}
import { hatchet } from './client';
import { corsair } from '@/server/corsair';

export const supportWorkflow = hatchet.workflow({
    name: 'slack-support-ticket',
    on: { event: 'slack:support.message' },
});

supportWorkflow.task('create-linear-issue', async (ctx) => {
    const { text, userId, channel, threadTs, tenantId } = ctx.workflowInput<{
        text: string;
        userId: string;
        channel: string;
        threadTs: string;
        tenantId?: string;
    }>();

    const client = tenantId ? corsair.withTenant(tenantId) : corsair;

    // Create a Linear issue from the Slack message
    const issue = await client.linear.api.issues.create({
        title: text.slice(0, 80),
        description: `Reported via Slack by <@${userId}>:\n\n${text}`,
        teamId: process.env.LINEAR_SUPPORT_TEAM_ID!,
        labelIds: [process.env.LINEAR_SUPPORT_LABEL!],
    });

    // Reply in the same Slack thread to confirm
    await client.slack.api.messages.post({
        channel,
        thread_ts: threadTs,
        text: `✅ Ticket created: <${issue.data.url}|${issue.data.identifier}>`,
    });

    return { issueId: issue.data.id };
});
```

## Workflow

When commits are pushed to the main branch, run a multi-step Hatchet workflow: notify the team in Discord, update the Linear project status to reflect the deployment, then log the release in a tracking channel.

```ts corsair.ts theme={null}
import { hatchet } from '@/hatchet/client';

github({
    webhookHooks: {
        push: {
            after: async (ctx, result) => {
                const branch = result.data.ref.replace('refs/heads/', '');
                if (branch !== 'main') return; // only track main

                await hatchet.client.event.push('github:push.main', {
                    headCommit: result.data.head_commit?.message ?? '',
                    pusher: result.data.pusher.name,
                    compareUrl: result.data.compare,
                    commitCount: result.data.commits?.length ?? 0,
                    tenantId: ctx.tenantId,
                });
            },
        },
    },
})
```

```ts hatchet/workflows.ts theme={null}
import { hatchet } from './client';
import { corsair } from '@/server/corsair';

export const deployWorkflow = hatchet.workflow({
    name: 'main-branch-push',
    on: { event: 'github:push.main' },
});

type PushInput = {
    headCommit: string;
    pusher: string;
    compareUrl: string;
    commitCount: number;
    tenantId?: string;
};

deployWorkflow.task('notify-discord', async (ctx) => {
    const input = ctx.workflowInput<PushInput>();
    const client = input.tenantId ? corsair.withTenant(input.tenantId) : corsair;

    await client.discord.api.messages.create({
        channelId: process.env.DISCORD_DEPLOYS_CHANNEL!,
        content: `🚀 **${input.commitCount} commit(s)** pushed to \`main\` by **${input.pusher}**\n${input.headCommit}\n[View diff](${input.compareUrl})`,
    });
});

deployWorkflow.task('update-linear-project', async (ctx) => {
    const input = ctx.workflowInput<PushInput>();
    const client = input.tenantId ? corsair.withTenant(input.tenantId) : corsair;

    // Move any Linear issues marked "In Review" to "Done" (fields live on .data).
    // Rows store flat stateId/teamId, not nested objects. Compare against ids.
    const all = await client.linear.db.issues.list();
    const inReview = all.filter(
        (i) => i.data.stateId === process.env.LINEAR_IN_REVIEW_STATE_ID && i.data.teamId === process.env.LINEAR_TEAM_ID,
    );

    for (const issue of inReview) {
        await client.linear.api.issues.update({
            issueId: issue.data.id,
            stateId: process.env.LINEAR_DONE_STATE_ID!,
        });
    }
});

deployWorkflow.task('log-release', async (ctx) => {
    const input = ctx.workflowInput<PushInput>();
    const client = input.tenantId ? corsair.withTenant(input.tenantId) : corsair;

    await client.slack.api.messages.post({
        channel: 'C_RELEASES_CHANNEL',
        text: `🔖 Deployed to main: _${input.headCommit}_ by ${input.pusher}\n${input.compareUrl}`,
    });
});
```

## Cron job

Every Monday at 9 AM, run a Hatchet cron workflow that pulls the current sprint's Linear issues from Corsair's database and posts a structured report to Slack.

```ts hatchet/workflows.ts theme={null}
import { hatchet } from './client';
import { corsair } from '@/server/corsair';

export const sprintReportWorkflow = hatchet.workflow({
    name: 'weekly-sprint-report',
    on: { cron: '0 9 * * 1' }, // Every Monday at 9am UTC
});

sprintReportWorkflow.task('post-sprint-report', async () => {
    // List once, then filter/sort in JS (fields live on .data).
    // Rows store flat stateId/assigneeId, not nested objects. Compare against ids.
    const all = await corsair.linear.db.issues.list();
    const byPriority = (a, b) => (a.data.priority ?? 0) - (b.data.priority ?? 0);
    const unstartedStateIds = process.env.LINEAR_UNSTARTED_STATE_IDS!.split(',');

    const inProgress = all
        .filter((i) => i.data.stateId === process.env.LINEAR_IN_PROGRESS_STATE_ID)
        .sort(byPriority);
    const blocked = all.filter((i) => i.data.stateId === process.env.LINEAR_BLOCKED_STATE_ID);
    const unstarted = all
        .filter((i) => unstartedStateIds.includes(i.data.stateId))
        .sort(byPriority)
        .slice(0, 5);

    // Rows carry assigneeId, not names. Resolve the related record if you need names.
    const lines = [
        `*Sprint Report — ${new Date().toDateString()}*`,
        '',
        `*🔄 In Progress (${inProgress.length})*`,
        ...inProgress.map((i) => `• *${i.data.title}* — ${i.data.assigneeId ?? 'Unassigned'}`),
        '',
        `*🚧 Blocked (${blocked.length})*`,
        ...blocked.map((i) => `• *${i.data.title}* — ${i.data.assigneeId ?? 'Unassigned'}`),
        '',
        `*📋 Up Next (${unstarted.length})*`,
        ...unstarted.map((i) => `• ${i.data.title}`),
    ];

    await corsair.slack.api.messages.post({
        channel: 'C_ENG_CHANNEL',
        text: lines.join('\n'),
    });
});
```

```ts hatchet/worker.ts theme={null}
import { hatchet } from './client';
import { supportWorkflow, deployWorkflow, sprintReportWorkflow } from './workflows';

const worker = await hatchet.worker('corsair-worker');

worker.registerWorkflow(supportWorkflow);
worker.registerWorkflow(deployWorkflow);
worker.registerWorkflow(sprintReportWorkflow);

await worker.start();
```

<Info>
  Register all workflows in a single worker. The cron schedule is part of the workflow definition. Hatchet picks it up automatically when the worker connects.
</Info>

## What's next

<CardGroup cols={2}>
  <Card title="Inngest" href="/guides/inngest">
    Durable step functions triggered from Corsair webhooks.
  </Card>

  <Card title="Temporal" href="/guides/temporal">
    Start Temporal workflows from Corsair webhook events.
  </Card>

  <Card title="Trigger.dev" href="/guides/trigger-dev">
    Background tasks and scheduled jobs with Trigger.dev.
  </Card>

  <Card title="Workflows guide" href="/guides/workflows">
    Chain webhook events without a job queue.
  </Card>
</CardGroup>
