diff --git a/.changeset/strange-gorillas-kick.md b/.changeset/strange-gorillas-kick.md
new file mode 100644
index 00000000000..c8931555085
--- /dev/null
+++ b/.changeset/strange-gorillas-kick.md
@@ -0,0 +1,6 @@
+---
+"@trigger.dev/sdk": patch
+"@trigger.dev/linear": patch
+---
+
+First release of `@trigger.dev/linear` integration. `io.runTask()` error handlers can now prevent further retries.
diff --git a/.env.example b/.env.example
index 295b41745c7..48170e8f365 100644
--- a/.env.example
+++ b/.env.example
@@ -31,6 +31,8 @@ CLOUD_AIRTABLE_CLIENT_ID=
CLOUD_AIRTABLE_CLIENT_SECRET=
CLOUD_GITHUB_CLIENT_ID=
CLOUD_GITHUB_CLIENT_SECRET=
+CLOUD_LINEAR_CLIENT_ID=
+CLOUD_LINEAR_CLIENT_SECRET=
CLOUD_SLACK_APP_HOST=
CLOUD_SLACK_CLIENT_ID=
CLOUD_SLACK_CLIENT_SECRET=
\ No newline at end of file
diff --git a/apps/webapp/app/services/externalApis/integrationCatalog.server.ts b/apps/webapp/app/services/externalApis/integrationCatalog.server.ts
index 4369ad87472..b86c2e496ce 100644
--- a/apps/webapp/app/services/externalApis/integrationCatalog.server.ts
+++ b/apps/webapp/app/services/externalApis/integrationCatalog.server.ts
@@ -1,5 +1,6 @@
import { airtable } from "./integrations/airtable";
import { github } from "./integrations/github";
+import { linear } from "./integrations/linear";
import { openai } from "./integrations/openai";
import { plain } from "./integrations/plain";
import { resend } from "./integrations/resend";
@@ -33,6 +34,7 @@ export class IntegrationCatalog {
export const integrationCatalog = new IntegrationCatalog({
airtable,
github,
+ linear,
openai,
plain,
resend,
diff --git a/apps/webapp/app/services/externalApis/integrations/linear.ts b/apps/webapp/app/services/externalApis/integrations/linear.ts
new file mode 100644
index 00000000000..355d968833b
--- /dev/null
+++ b/apps/webapp/app/services/externalApis/integrations/linear.ts
@@ -0,0 +1,104 @@
+import type { HelpSample, Integration } from "../types";
+
+function usageSample(hasApiKey: boolean): HelpSample {
+ return {
+ title: "Using the client",
+ code: `
+import { Linear } from "@trigger.dev/linear";
+
+const linear = new Linear({
+ id: "__SLUG__",${hasApiKey ? ",\n apiKey: process.env.LINEAR_API_KEY!" : ""}
+});
+
+client.defineJob({
+ id: "linear-react-to-new-issue",
+ name: "Linear - React To New Issue",
+ version: "0.1.0",
+ integrations: { linear },
+ trigger: linear.onIssueCreated(),
+ run: async (payload, io, ctx) => {
+ await io.linear.createComment("create-comment", {
+ issueId: payload.data.id,
+ body: "Thank's for opening this issue!"
+ });
+
+ await io.linear.createReaction("create-reaction", {
+ issueId: payload.data.id,
+ emoji: "+1"
+ });
+
+ return { payload, ctx };
+ },
+});
+ `,
+ };
+}
+
+export const linear: Integration = {
+ identifier: "linear",
+ name: "Linear",
+ packageName: "@trigger.dev/linear@latest",
+ authenticationMethods: {
+ oauth2: {
+ name: "OAuth",
+ type: "oauth2",
+ client: {
+ id: {
+ envName: "CLOUD_LINEAR_CLIENT_ID",
+ },
+ secret: {
+ envName: "CLOUD_LINEAR_CLIENT_SECRET",
+ },
+ },
+ config: {
+ authorization: {
+ url: "https://linear.app/oauth/authorize",
+ scopeSeparator: ",",
+ },
+ token: {
+ url: "https://api.linear.app/oauth/token",
+ metadata: {},
+ },
+ refresh: {
+ url: "https://linear.app/oauth/authorize",
+ },
+ pkce: false,
+ },
+ scopes: [
+ {
+ name: "write",
+ description:
+ "Grants global write access to the user's account. Use a more targeted scope if you don't need full access.",
+ defaultChecked: true,
+ },
+
+ {
+ name: "issue:create",
+ description: "Grants access to create issues and attachments only.",
+ annotations: [{ label: "Issues" }],
+ },
+
+ {
+ name: "comments:create",
+ description: "Grants access to create new issue comments.",
+ annotations: [{ label: "Comments" }],
+ },
+
+ {
+ name: "admin",
+ description:
+ "Grants full access to admin-level endpoints. Don't use this unless you really need it.",
+ },
+ ],
+ help: {
+ samples: [usageSample(false)],
+ },
+ },
+ apikey: {
+ type: "apikey",
+ help: {
+ samples: [usageSample(true)],
+ },
+ },
+ },
+};
diff --git a/docs/integrations/apis/linear.mdx b/docs/integrations/apis/linear.mdx
new file mode 100644
index 00000000000..5c90f75cc42
--- /dev/null
+++ b/docs/integrations/apis/linear.mdx
@@ -0,0 +1,479 @@
+---
+title: Linear
+description: "Streamline your project and issue tracking"
+---
+
+
+
+## Installation
+
+To get started with the Linear integration on Trigger.dev, you need to install the `@trigger.dev/linear` package.
+You can do this using npm, pnpm, or yarn:
+
+
+
+```bash npm
+npm install @trigger.dev/linear@latest
+```
+
+```bash pnpm
+pnpm add @trigger.dev/linear@latest
+```
+
+```bash yarn
+yarn add @trigger.dev/linear@latest
+```
+
+
+
+## Authentication
+
+To use the Linear API with Trigger.dev, you can either use OAuth or a Personal API Key.
+
+### OAuth
+
+```ts
+import { Linear } from "@trigger.dev/linear";
+
+//this will use OAuth
+const linear = new Linear({
+ id: "linear",
+});
+```
+
+### Personal API Key
+
+You can create a Personal API Key in your [Linear API Settings](https://linear.app/settings/api).
+
+```ts
+import { Linear } from "@trigger.dev/linear";
+
+//this will use the passed in API key (defined in your environment variables)
+const linear = new Linear({
+ id: "linear",
+ apiKey: process.env["LINEAR_API_KEY"],
+});
+```
+
+## Usage
+
+Include the Linear integration in your Trigger.dev job.
+
+```ts
+client.defineJob({
+ id: "linear-new-issue-autoresponder",
+ name: "Linear - New Issue Autoresponder",
+ version: "0.1.0",
+ integrations: {
+ //use the linear integration
+ linear,
+ },
+ //trigger on issue created events
+ trigger: linear.onIssueCreated(),
+ run: async (payload, io, ctx) => {
+ //get new issue ID from the event payload
+ const newIssueId = payload.data.id;
+
+ //comment
+ await io.linear.createComment("create-comment", {
+ issueId: newIssueId,
+ body: "Thank's for opening this issue!",
+ });
+
+ //react
+ await io.linear.createReaction("create-reaction", {
+ issueId: newIssueId,
+ emoji: "+1",
+ });
+
+ //store and display in the job run
+ return { payload, ctx };
+ },
+});
+```
+
+### Serialization helper
+
+Use the `serializeLinearOutput` helper instead of returning raw Linear SDK responses:
+
+```ts
+import { Linear, serializeLinearOutput } from "@trigger.dev/linear";
+...
+client.defineJob({
+ id: "linear-sdk",
+ name: "Linear SDK",
+ version: "0.1.0",
+ integrations: {
+ linear,
+ },
+ trigger: eventTrigger({
+ name: "linear.sdk",
+ }),
+ run: async (payload, io, ctx) => {
+ //the official Linear SDK is exposed as `client`
+ const issues = await io.linear.runTask("first-two", async (client) => {
+ //these nodes contain values we can't serialize, e.g. functions
+ const { nodes } = await client.issues({ first: 2 });
+ //we remove them with this little helper
+ return serializeLinearOutput(nodes);
+ });
+ return issues;
+ },
+});
+```
+
+### Pagination
+
+You can paginate responses three different ways:
+
+1. Via the raw Linear SDK exposed in `io.runTask()`
+2. Iterating the same integration task with different params
+3. Using the `getAll` helper exposed on the integration (**recommended!**)
+
+_When ordering results, make sure to use the `PaginationOrderBy` enum._
+
+```ts
+import { Linear, PaginationOrderBy, serializeLinearOutput } from "@trigger.dev/linear";
+...
+client.defineJob({
+ id: "linear-pagination",
+ name: "Linear Pagination",
+ version: "0.1.0",
+ integrations: {
+ linear,
+ },
+ trigger: eventTrigger({
+ name: "linear.paginate",
+ }),
+ run: async (payload, io, ctx) => {
+ //the same params will be used for all tasks
+ const params = { first: 5, orderBy: PaginationOrderBy.UpdatedAt };
+
+ //1. Linear SDK
+ const sdkIssues = await io.linear.runTask("all-issues-via-sdk", async (client) => {
+ const edges = await client.issues(params);
+
+ //this will keep appending nodes until there are no more
+ while (edges.pageInfo.hasNextPage) {
+ await edges.fetchNext();
+ }
+
+ //use serialization helper to remove functions etc
+ return serializeLinearOutput(edges.nodes);
+ });
+
+ //2. Linear integration - no pagination helper
+ let edges = await io.linear.issues("get-issues", params);
+ let noHelper = edges.nodes;
+
+ for (let i = 0; edges.pageInfo.hasNextPage; i++) {
+ edges = await io.linear.issues(`get-more-issues-${i}`, {
+ ...params,
+ after: edges.pageInfo.endCursor,
+ });
+ noHelper = noHelper.concat(edges.nodes);
+ }
+
+ //3. Linear integration - with the pagination helper
+ const withHelper = await io.linear.getAll(io.linear.issues, "get-all", params);
+
+ return {
+ issueCounts: {
+ withSdk: sdkIssues.length,
+ noHelper: noHelper.length,
+ withHelper: withHelper.length,
+ },
+ };
+ },
+});
+```
+
+## Triggers
+
+### Attachments
+
+| Function Name | Description |
+| --------------------- | ---------------------------------------------- |
+| `onAttachment` | When any action is performed on an attachment. |
+| `onAttachmentCreated` | When an attachment is created. |
+| `onAttachmentRemoved` | When an attachment is removed. |
+| `onAttachmentUpdated` | When an attachment is updated. |
+
+### Comments
+
+| Function Name | Description |
+| ------------------ | ------------------------------------------- |
+| `onComment` | When any action is performed on an comment. |
+| `onCommentCreated` | When an comment is created. |
+| `onCommentRemoved` | When an comment is removed. |
+| `onCommentUpdated` | When an comment is updated. |
+
+### Cycles
+
+| Function Name | Description |
+| ---------------- | ----------------------------------------- |
+| `onCycle` | When any action is performed on an cycle. |
+| `onCycleCreated` | When an cycle is created. |
+| `onCycleRemoved` | When an cycle is removed. |
+| `onCycleUpdated` | When an cycle is updated. |
+
+### Issues
+
+| Function Name | Description |
+| ---------------- | ----------------------------------------- |
+| `onIssue` | When any action is performed on an issue. |
+| `onIssueCreated` | When an issue is created. |
+| `onIssueRemoved` | When an issue is removed. |
+| `onIssueUpdated` | When an issue is updated. |
+
+### Issue Labels
+
+| Function Name | Description |
+| --------------------- | ----------------------------------------------- |
+| `onIssueLabel` | When any action is performed on an issue label. |
+| `onIssueLabelCreated` | When an issue label is created. |
+| `onIssueLabelRemoved` | When an issue label is removed. |
+| `onIssueLabelUpdated` | When an issue label is updated. |
+
+### Issue SLAs
+
+| Function Name | Description |
+| -------------------- | --------------------------------------------- |
+| `onIssueSLA` | When any action is performed on an issue SLA. |
+| `onIssueSLASet` | When an issue SLA is set. |
+| `onIssueSLABreached` | When an issue SLA is breached. |
+| `onIssueSLAHighRisk` | When an issue SLA is high risk. |
+
+### Projects
+
+| Function Name | Description |
+| ------------------ | ------------------------------------------- |
+| `onProject` | When any action is performed on an project. |
+| `onProjectCreated` | When an project is created. |
+| `onProjectRemoved` | When an project is removed. |
+| `onProjectUpdated` | When an project is updated. |
+
+### Project Updates
+
+| Function Name | Description |
+| ------------------------ | -------------------------------------------------- |
+| `onProjectUpdate` | When any action is performed on an project update. |
+| `onProjectUpdateCreated` | When an project update is created. |
+| `onProjectUpdateRemoved` | When an project update is removed. |
+| `onProjectUpdateUpdated` | When an project update is updated. |
+
+### Reactions
+
+| Function Name | Description |
+| ------------------- | -------------------------------------------- |
+| `onReaction` | When any action is performed on an reaction. |
+| `onReactionCreated` | When an reaction is created. |
+| `onReactionRemoved` | When an reaction is removed. |
+| `onReactionUpdated` | When an reaction is updated. |
+
+## Tasks
+
+### Attachments
+
+| Function Name | Description |
+| ------------------ | -------------------------- |
+| `attachment` | Gets an attachment. |
+| `attachments` | Gets multiple attachments. |
+| `createAttachment` | Creates an attachment. |
+| `deleteAttachment` | Deletes an attachment. |
+| `updateAttachment` | Updates an attachment. |
+
+### Attachment Links
+
+| Function Name | Description |
+| ------------------------- | ------------------------------------------ |
+| `attachmentLinkFront` | Links a Front conversation to an issue. |
+| `attachmentLinkIntercom` | Links a Intercom conversation to an issue. |
+| `attachmentLinkJiraIssue` | Links a Jira issue to an issue. |
+| `attachmentLinkSlack` | Links a Slack message to an issue. |
+| `attachmentLinkURL` | Links any URL to an issue. |
+| `attachmentLinkZendesk` | Links a Zendesk ticket to an issue. |
+
+### Comments
+
+| Function Name | Description |
+| --------------- | ----------------------- |
+| `comment` | Gets a comment. |
+| `comments` | Gets multiple comments. |
+| `createComment` | Creates a comment. |
+| `deleteComment` | Deletes a comment. |
+| `updateComment` | Updates a comment. |
+
+### Cycles
+
+| Function Name | Description |
+| -------------- | ----------------- |
+| `archiveCycle` | Archives a cycle. |
+| `createCycle` | Creates a cycle. |
+| `updateCycle` | Updates a cycle. |
+
+### Documents
+
+| Function Name | Description |
+| ----------------- | ------------------------ |
+| `document` | Gets a document. |
+| `documents` | Gets multiple documents. |
+| `createDocument` | Creates a document. |
+| `searchDocuments` | Searches documents. |
+
+### Favorites
+
+| Function Name | Description |
+| ---------------- | ------------------------ |
+| `favorite` | Gets a favorite. |
+| `favorites` | Gets multiple favorites. |
+| `createFavorite` | Creates a favorite. |
+
+### Issues
+
+| Function Name | Description |
+| -------------- | --------------------- |
+| `issue` | Gets an issue. |
+| `issues` | Gets multiple issues. |
+| `archiveIssue` | Archives an issue. |
+| `createIssue` | Creates an issue. |
+| `deleteIssue` | Deletes an issue. |
+| `searchIssues` | Searches issues. |
+| `updateIssue` | Updates an issue. |
+
+### Issue Labels
+
+| Function Name | Description |
+| ------------------ | --------------------------- |
+| `issueLabel` | Gets an issue label. |
+| `issueLabels` | Gets multiple issue labels. |
+| `createIssueLabel` | Creates an issue label. |
+| `deleteIssueLabel` | Deletes an issue label. |
+| `updateIssueLabel` | Updates an issue label. |
+
+### Issue Relations
+
+| Function Name | Description |
+| --------------------- | ------------------------------ |
+| `issueRelation` | Gets an issue relation. |
+| `issueRelations` | Gets multiple issue relations. |
+| `createIssueRelation` | Creates an issue relation. |
+
+### Notifications
+
+| Function Name | Description |
+| -------------------------------- | ------------------------------------ |
+| `notification` | Gets a notification. |
+| `notifications` | Gets multiple notifications. |
+| `archiveNotification` | Archives a notification. |
+| `createNotificationSubscription` | Creates a notification subscription. |
+
+### Organizations
+
+| Function Name | Description |
+| ---------------------------------- | ------------------------------- |
+| `organization` | Gets the viewer's organization. |
+| `createOrganizationFromOnboarding` | Creates an organization. |
+| `createOrganizationInvite` | Creates an organization invite. |
+
+### Projects
+
+| Function Name | Description |
+| ---------------- | ----------------------- |
+| `project` | Gets a project. |
+| `projects` | Gets multiple projects. |
+| `archiveProject` | Archives a project. |
+| `createProject` | Creates a project. |
+| `deleteProject` | Deletes a project. |
+| `searchProjects` | Searches projects. |
+| `updateProject` | Updates a project. |
+
+### Project Links
+
+| Function Name | Description |
+| ------------------- | ---------------------------- |
+| `projectLink` | Gets a project link. |
+| `projectLinks` | Gets multiple project links. |
+| `createProjectLink` | Creates a project link. |
+
+### Project Updates
+
+| Function Name | Description |
+| --------------------- | ------------------------------ |
+| `projectUpdate` | Gets a project update. |
+| `projectUpdates` | Gets multiple project updates. |
+| `createProjectUpdate` | Creates a project update. |
+| `deleteProjectUpdate` | Deletes a project update. |
+| `updateProjectUpdate` | Updates a project update. |
+
+### Reactions
+
+| Function Name | Description |
+| ---------------- | ------------------- |
+| `createReaction` | Creates a reaction. |
+| `deleteReaction` | Deletes a reaction. |
+
+### Roadmaps
+
+| Function Name | Description |
+| ---------------- | ------------------- |
+| `archiveRoadmap` | Archives a roadmap. |
+| `createRoadmap` | Creates a roadmap. |
+
+### Teams
+
+| Function Name | Description |
+| ------------- | -------------------- |
+| `team` | Gets a team. |
+| `teams` | Gets multiple teams. |
+| `createTeam` | Creates a team. |
+
+### Team Memberships
+
+| Function Name | Description |
+| ---------------------- | ------------------------------- |
+| `teamMembership` | Gets a team membership. |
+| `teamMemberships` | Gets multiple team memberships. |
+| `createTeamMembership` | Creates a team membership. |
+
+### Templates
+
+| Function Name | Description |
+| ------------- | ------------------------ |
+| `template` | Gets a template. |
+| `templates` | Gets multiple templates. |
+
+### Users
+
+| Function Name | Description |
+| ------------- | -------------------- |
+| `user` | Gets a user. |
+| `users` | Gets multiple users. |
+| `updateUser` | Updates a user. |
+
+### Webhooks
+
+| Function Name | Description |
+| --------------- | ----------------------- |
+| `webhook` | Gets a webhook. |
+| `webhooks` | Gets multiple webhooks. |
+| `createWebhook` | Creates a webhook. |
+| `deleteWebhook` | Deletes a webhook. |
+| `updateWebhook` | Updates a webhook. |
+
+### Workflow States
+
+| Function Name | Description |
+| ---------------------- | ------------------------------ |
+| `workflowState` | Gets a workflow state. |
+| `workflowStates` | Gets multiple workflow states. |
+| `archiveWorkflowState` | Archives a workflow state. |
+| `createWorkflowState` | Creates a workflow state. |
+
+### Misc
+
+| Function Name | Description |
+| ------------------------ | -------------------------------------- |
+| `createProjectMilestone` | Creates a project milestone. |
+| `issuePriorityValues` | Gets issue priority values and labels. |
+| `viewer` | Gets the currently authenticated user. |
diff --git a/docs/integrations/introduction.mdx b/docs/integrations/introduction.mdx
index 1496d2e69a2..30be9c1e365 100644
--- a/docs/integrations/introduction.mdx
+++ b/docs/integrations/introduction.mdx
@@ -33,6 +33,7 @@ Navigate the menu or select Integrations from the table below.
| API | Description | Webhooks | Tasks |
| --------------------------------------- | ---------------------------------------------------------------- | -------- | ----- |
| [GitHub](/integrations/apis/github) | Subscribe to webhooks and perform actions | ✅ | ✅ |
+| [Linear](/integrations/apis/linear) | Streamline project and issue tracking | ✅ | ✅ |
| [OpenAI](/integrations/apis/openai) | Generate text and images. Including longer than 30s prompts | N/A | ✅ |
| [Plain](/integrations/apis/plain) | Perform customer support using Plain | 🕘 | ✅ |
| [Resend](/integrations/apis/resend) | Send emails using Resend | 🕘 | ✅ |
diff --git a/docs/mint.json b/docs/mint.json
index f8f2b7cf851..81c5dfb1717 100644
--- a/docs/mint.json
+++ b/docs/mint.json
@@ -244,6 +244,7 @@
"integrations/apis/github-tasks"
]
},
+ "integrations/apis/linear",
"integrations/apis/openai",
"integrations/apis/plain",
"integrations/apis/resend",
diff --git a/integrations/linear/README.md b/integrations/linear/README.md
new file mode 100644
index 00000000000..13ffab69b4a
--- /dev/null
+++ b/integrations/linear/README.md
@@ -0,0 +1,3 @@
+
+# @trigger.dev/linear
+
\ No newline at end of file
diff --git a/integrations/linear/package.json b/integrations/linear/package.json
new file mode 100644
index 00000000000..c65a4a586f6
--- /dev/null
+++ b/integrations/linear/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "@trigger.dev/linear",
+ "version": "2.1.3",
+ "description": "Trigger.dev integration for @linear/sdk",
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "publishConfig": {
+ "access": "public"
+ },
+ "files": [
+ "dist/index.js",
+ "dist/index.d.ts",
+ "dist/index.js.map"
+ ],
+ "devDependencies": {
+ "@types/node": "16.x",
+ "rimraf": "^3.0.2",
+ "tsup": "7.1.x",
+ "typescript": "4.9.4"
+ },
+ "scripts": {
+ "clean": "rimraf dist",
+ "build": "npm run clean && npm run build:tsup",
+ "build:tsup": "tsup",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@linear/sdk": "^8.0.0",
+ "@trigger.dev/integration-kit": "workspace:^2.1.0",
+ "@trigger.dev/sdk": "workspace:^2.1.0",
+ "zod": "3.21.4"
+ },
+ "engines": {
+ "node": ">=16.8.0"
+ }
+}
\ No newline at end of file
diff --git a/integrations/linear/src/events.ts b/integrations/linear/src/events.ts
new file mode 100644
index 00000000000..18dff27b58b
--- /dev/null
+++ b/integrations/linear/src/events.ts
@@ -0,0 +1,535 @@
+import { EventSpecification } from "@trigger.dev/sdk";
+import {
+ AttachmentEvent,
+ CommentEvent,
+ CycleEvent,
+ IssueEvent,
+ IssueLabelEvent,
+ IssueSLAEvent,
+ ProjectEvent,
+ ProjectUpdateEvent,
+ ReactionEvent,
+} from "./schemas";
+import { GetLinearPayload } from "./types";
+import {
+ attachmentCreated,
+ attachmentRemoved,
+ attachmentUpdated,
+ commentCreated,
+ commentRemoved,
+ commentUpdated,
+ cycleCreated,
+ cycleRemoved,
+ cycleUpdated,
+ issueCreated,
+ issueRemoved,
+ issueUpdated,
+ issueLabelCreated,
+ issueLabelRemoved,
+ issueLabelUpdated,
+ projectCreated,
+ projectRemoved,
+ projectUpdated,
+ projectUpdateCreated,
+ projectUpdateRemoved,
+ projectUpdateUpdated,
+ reactionCreated,
+ reactionRemoved,
+ reactionUpdated,
+} from "./payload-examples";
+import { onCommentProperties, onIssueProperties, updatedFromProperties } from "./utils";
+
+/** **WARNING:** Still in alpha - use with caution! */
+export const onAttachment: EventSpecification> = {
+ name: "Attachment",
+ title: "On Attachment",
+ source: "linear.app",
+ icon: "linear",
+ examples: [attachmentCreated, attachmentRemoved, attachmentUpdated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [
+ { label: "Event action", text: payload.action },
+ { label: "Attachment ID", text: payload.data.id },
+ ],
+};
+
+/** **WARNING:** Still in alpha - use with caution! */
+export const onAttachmentCreated: EventSpecification> =
+ {
+ name: "Attachment",
+ title: "On Attachment Created",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["create"],
+ },
+ examples: [attachmentCreated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [{ label: "Attachment ID", text: payload.data.id }],
+ };
+
+/** **WARNING:** Still in alpha - use with caution! */
+export const onAttachmentRemoved: EventSpecification> =
+ {
+ name: "Attachment",
+ title: "On Attachment Removed",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["remove"],
+ },
+ examples: [attachmentRemoved],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [{ label: "Attachment ID", text: payload.data.id }],
+ };
+
+/** **WARNING:** Still in alpha - use with caution! */
+export const onAttachmentUpdated: EventSpecification> =
+ {
+ name: "Attachment",
+ title: "On Attachment Updated",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["update"],
+ },
+ examples: [attachmentUpdated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [{ label: "Attachment ID", text: payload.data.id }],
+ };
+
+export const onComment: EventSpecification> = {
+ name: "Comment",
+ title: "On Comment",
+ source: "linear.app",
+ icon: "linear",
+ examples: [commentCreated, commentRemoved, commentUpdated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [
+ { label: "Event action", text: payload.action },
+ ...onCommentProperties(payload),
+ ...updatedFromProperties(payload),
+ ],
+};
+
+export const onCommentCreated: EventSpecification> = {
+ name: "Comment",
+ title: "On Comment Created",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["create"],
+ },
+ examples: [commentCreated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => onCommentProperties(payload),
+};
+
+export const onCommentRemoved: EventSpecification> = {
+ name: "Comment",
+ title: "On Comment Removed",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["remove"],
+ },
+ examples: [commentRemoved],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => onCommentProperties(payload),
+};
+
+export const onCommentUpdated: EventSpecification> = {
+ name: "Comment",
+ title: "On Comment Updated",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["update"],
+ },
+ examples: [commentUpdated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [...onCommentProperties(payload), ...updatedFromProperties(payload)],
+};
+
+export const onCycle: EventSpecification> = {
+ name: "Cycle",
+ title: "On Cycle",
+ source: "linear.app",
+ icon: "linear",
+ examples: [cycleCreated, cycleRemoved, cycleUpdated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [
+ { label: "Event action", text: payload.action },
+ { label: "Cycle ID", text: payload.data.id },
+ ],
+};
+
+export const onCycleCreated: EventSpecification> = {
+ name: "Cycle",
+ title: "On Cycle Created",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["create"],
+ },
+ examples: [cycleCreated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [{ label: "Cycle ID", text: payload.data.id }],
+};
+
+export const onCycleRemoved: EventSpecification> = {
+ name: "Cycle",
+ title: "On Cycle Removed",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["remove"],
+ },
+ examples: [cycleRemoved],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [{ label: "Cycle ID", text: payload.data.id }],
+};
+
+export const onCycleUpdated: EventSpecification> = {
+ name: "Cycle",
+ title: "On Cycle Updated",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["update"],
+ },
+ examples: [cycleUpdated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [{ label: "Cycle ID", text: payload.data.id }],
+};
+
+export const onIssue: EventSpecification> = {
+ name: "Issue",
+ title: "On Issue",
+ source: "linear.app",
+ icon: "linear",
+ examples: [issueCreated, issueRemoved, issueUpdated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [
+ { label: "Event action", text: payload.action },
+ ...onIssueProperties(payload),
+ ...updatedFromProperties(payload),
+ ],
+};
+
+export const onIssueCreated: EventSpecification> = {
+ name: "Issue",
+ title: "On Issue Created",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["create"],
+ },
+ examples: [issueCreated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => onIssueProperties(payload),
+};
+
+export const onIssueRemoved: EventSpecification> = {
+ name: "Issue",
+ title: "On Issue Removed",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["remove"],
+ },
+ examples: [issueRemoved],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => onIssueProperties(payload),
+};
+
+export const onIssueUpdated: EventSpecification> = {
+ name: "Issue",
+ title: "On Issue Updated",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["update"],
+ },
+ examples: [issueUpdated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [...onIssueProperties(payload), ...updatedFromProperties(payload)],
+};
+
+export const onIssueLabel: EventSpecification> = {
+ name: "IssueLabel",
+ title: "On IssueLabel",
+ source: "linear.app",
+ icon: "linear",
+ examples: [issueLabelCreated, issueLabelRemoved, issueLabelUpdated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [
+ { label: "Event action", text: payload.action },
+ { label: "IssueLabel ID", text: payload.data.id },
+ ],
+};
+
+export const onIssueLabelCreated: EventSpecification> =
+ {
+ name: "IssueLabel",
+ title: "On IssueLabel Created",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["create"],
+ },
+ examples: [issueLabelCreated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [{ label: "IssueLabel ID", text: payload.data.id }],
+ };
+
+export const onIssueLabelRemoved: EventSpecification> =
+ {
+ name: "IssueLabel",
+ title: "On IssueLabel Removed",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["remove"],
+ },
+ examples: [issueLabelRemoved],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [{ label: "IssueLabel ID", text: payload.data.id }],
+ };
+
+export const onIssueLabelUpdated: EventSpecification> =
+ {
+ name: "IssueLabel",
+ title: "On IssueLabel Updated",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["update"],
+ },
+ examples: [issueLabelUpdated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [{ label: "IssueLabel ID", text: payload.data.id }],
+ };
+
+// TODO: this needs to be tested
+export const onIssueSLA: EventSpecification> = {
+ name: "IssueSLA",
+ title: "On Issue SLA",
+ source: "linear.app",
+ icon: "linear",
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [
+ { label: "SLA action", text: payload.action },
+ { label: "Issue ID", text: payload.issueData.id },
+ ],
+};
+
+export const onIssueSLASet: EventSpecification> = {
+ name: "IssueSLA",
+ title: "On Issue SLA Set",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["set"],
+ },
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [{ label: "Issue ID", text: payload.issueData.id }],
+};
+
+export const onIssueSLABreached: EventSpecification> = {
+ name: "IssueSLA",
+ title: "On Issue SLA Breached",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["breached"],
+ },
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [{ label: "Issue ID", text: payload.issueData.id }],
+};
+
+export const onIssueSLAHighRisk: EventSpecification> = {
+ name: "IssueSLA",
+ title: "On Issue SLA High Risk",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["highRisk"],
+ },
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [{ label: "Issue ID", text: payload.issueData.id }],
+};
+
+export const onProject: EventSpecification> = {
+ name: "Project",
+ title: "On Project",
+ source: "linear.app",
+ icon: "linear",
+ examples: [projectCreated, projectRemoved, projectUpdated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [
+ { label: "Event action", text: payload.action },
+ { label: "Project ID", text: payload.data.id },
+ { label: "Project Name", text: payload.data.name, url: payload.url ?? undefined },
+ ...updatedFromProperties(payload),
+ ],
+};
+
+export const onProjectCreated: EventSpecification> = {
+ name: "Project",
+ title: "On Project Created",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["create"],
+ },
+ examples: [projectCreated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [
+ { label: "Project ID", text: payload.data.id },
+ { label: "Project Name", text: payload.data.name, url: payload.url ?? undefined },
+ ],
+};
+
+export const onProjectRemoved: EventSpecification> = {
+ name: "Project",
+ title: "On Project Removed",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["remove"],
+ },
+ examples: [projectRemoved],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [
+ { label: "Project ID", text: payload.data.id },
+ { label: "Project Name", text: payload.data.name, url: payload.url ?? undefined },
+ ],
+};
+
+export const onProjectUpdated: EventSpecification> = {
+ name: "Project",
+ title: "On Project Updated",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["update"],
+ },
+ examples: [projectUpdated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [
+ { label: "Project ID", text: payload.data.id },
+ { label: "Project Name", text: payload.data.name, url: payload.url ?? undefined },
+ ...updatedFromProperties(payload),
+ ],
+};
+
+export const onProjectUpdate: EventSpecification> = {
+ name: "ProjectUpdate",
+ title: "On ProjectUpdate",
+ source: "linear.app",
+ icon: "linear",
+ examples: [projectUpdateCreated, projectUpdateRemoved, projectUpdateUpdated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [
+ { label: "Event action", text: payload.action },
+ { label: "ProjectUpdate ID", text: payload.data.id },
+ ],
+};
+
+export const onProjectUpdateCreated: EventSpecification<
+ GetLinearPayload
+> = {
+ name: "ProjectUpdate",
+ title: "On ProjectUpdate Created",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["create"],
+ },
+ examples: [projectUpdateCreated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [{ label: "ProjectUpdate ID", text: payload.data.id }],
+};
+
+export const onProjectUpdateRemoved: EventSpecification<
+ GetLinearPayload
+> = {
+ name: "ProjectUpdate",
+ title: "On ProjectUpdate Removed",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["remove"],
+ },
+ examples: [projectUpdateRemoved],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [{ label: "ProjectUpdate ID", text: payload.data.id }],
+};
+
+export const onProjectUpdateUpdated: EventSpecification<
+ GetLinearPayload
+> = {
+ name: "ProjectUpdate",
+ title: "On ProjectUpdate Updated",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["update"],
+ },
+ examples: [projectUpdateUpdated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [{ label: "ProjectUpdate ID", text: payload.data.id }],
+};
+
+export const onReaction: EventSpecification> = {
+ name: "Reaction",
+ title: "On Reaction",
+ source: "linear.app",
+ icon: "linear",
+ examples: [reactionCreated, reactionRemoved, reactionUpdated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [
+ { label: "Event action", text: payload.action },
+ { label: "Reaction ID", text: payload.data.id },
+ ],
+};
+
+export const onReactionCreated: EventSpecification> = {
+ name: "Reaction",
+ title: "On Reaction Created",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["create"],
+ },
+ examples: [reactionCreated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [{ label: "Reaction ID", text: payload.data.id }],
+};
+
+export const onReactionRemoved: EventSpecification> = {
+ name: "Reaction",
+ title: "On Reaction Removed",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["remove"],
+ },
+ examples: [reactionRemoved],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [{ label: "Reaction ID", text: payload.data.id }],
+};
+
+export const onReactionUpdated: EventSpecification> = {
+ name: "Reaction",
+ title: "On Reaction Updated",
+ source: "linear.app",
+ icon: "linear",
+ filter: {
+ action: ["update"],
+ },
+ examples: [reactionUpdated],
+ parsePayload: (payload) => payload as GetLinearPayload,
+ runProperties: (payload) => [{ label: "Reaction ID", text: payload.data.id }],
+};
diff --git a/integrations/linear/src/index.ts b/integrations/linear/src/index.ts
new file mode 100644
index 00000000000..b9f7d218b90
--- /dev/null
+++ b/integrations/linear/src/index.ts
@@ -0,0 +1,2029 @@
+import {
+ ConnectionAuth,
+ IO,
+ IOTask,
+ IntegrationTaskKey,
+ Json,
+ Prettify,
+ RunTaskErrorCallback,
+ RunTaskOptions,
+ TriggerIntegration,
+ retry,
+} from "@trigger.dev/sdk";
+import {
+ Attachment,
+ AttachmentConnection,
+ AttachmentPayload,
+ Comment,
+ CommentConnection,
+ CommentPayload,
+ Connection,
+ CreateOrJoinOrganizationResponse,
+ CycleArchivePayload,
+ CyclePayload,
+ DeletePayload,
+ Document,
+ DocumentConnection,
+ DocumentPayload,
+ DocumentSearchPayload,
+ Favorite,
+ FavoriteConnection,
+ FavoritePayload,
+ FrontAttachmentPayload,
+ Issue,
+ IssueArchivePayload,
+ IssueConnection,
+ IssueLabel,
+ IssueLabelConnection,
+ IssueLabelPayload,
+ IssuePayload,
+ IssuePriorityValue,
+ IssueRelation,
+ IssueRelationConnection,
+ IssueRelationPayload,
+ IssueSearchPayload,
+ LinearClient,
+ LinearDocument as L,
+ LinearError,
+ Notification,
+ NotificationArchivePayload,
+ NotificationConnection,
+ NotificationSubscriptionPayload,
+ Organization,
+ OrganizationInvitePayload,
+ Project,
+ ProjectArchivePayload,
+ ProjectConnection,
+ ProjectLink,
+ ProjectLinkConnection,
+ ProjectLinkPayload,
+ ProjectMilestonePayload,
+ ProjectPayload,
+ ProjectSearchPayload,
+ ProjectUpdate,
+ ProjectUpdateConnection,
+ ProjectUpdatePayload,
+ RatelimitedLinearError,
+ ReactionPayload,
+ RoadmapArchivePayload,
+ RoadmapPayload,
+ Team,
+ TeamConnection,
+ TeamMembership,
+ TeamMembershipConnection,
+ TeamMembershipPayload,
+ TeamPayload,
+ Template,
+ User,
+ UserConnection,
+ UserPayload,
+ WorkflowState,
+ WorkflowStateArchivePayload,
+ WorkflowStateConnection,
+ WorkflowStatePayload,
+} from "@linear/sdk";
+
+import * as events from "./events";
+import { AwaitNested, LinearReturnType, SerializedLinearOutput } from "./types";
+import { Nullable, QueryVariables, queryProperties } from "./utils";
+import { TriggerParams, Webhooks, createTrigger, createWebhookEventSource } from "./webhooks";
+
+export type LinearIntegrationOptions = {
+ id: string;
+ apiKey?: string;
+};
+
+export type LinearRunTask = InstanceType["runTask"];
+
+export class Linear implements TriggerIntegration {
+ private _options: LinearIntegrationOptions;
+ private _client?: LinearClient;
+ private _io?: IO;
+ private _connectionKey?: string;
+
+ constructor(private options: LinearIntegrationOptions) {
+ if (Object.keys(options).includes("apiKey") && !options.apiKey) {
+ throw `Can't create Linear integration (${options.id}) as apiKey was undefined`;
+ }
+
+ this._options = options;
+ }
+
+ get authSource() {
+ return this._options.apiKey ? "LOCAL" : "HOSTED";
+ }
+
+ get id() {
+ return this._options.id;
+ }
+
+ get metadata() {
+ return { id: "linear", name: "Linear" };
+ }
+
+ get source() {
+ return createWebhookEventSource(this);
+ }
+
+ cloneForRun(io: IO, connectionKey: string, auth?: ConnectionAuth) {
+ const linear = new Linear(this._options);
+ linear._io = io;
+ linear._connectionKey = connectionKey;
+ linear._client = this.createClient(auth);
+ return linear;
+ }
+
+ createClient(auth?: ConnectionAuth) {
+ if (auth) {
+ return new LinearClient({
+ accessToken: auth.accessToken,
+ });
+ }
+
+ if (this._options.apiKey) {
+ return new LinearClient({
+ apiKey: this._options.apiKey,
+ });
+ }
+
+ throw new Error("No auth");
+ }
+
+ runTask | void>(
+ key: IntegrationTaskKey,
+ callback: (client: LinearClient, task: IOTask, io: IO) => Promise,
+ options?: RunTaskOptions,
+ errorCallback?: RunTaskErrorCallback
+ ): Promise {
+ if (!this._io) throw new Error("No IO");
+ if (!this._connectionKey) throw new Error("No connection key");
+
+ return this._io.runTask(
+ key,
+ (task, io) => {
+ if (!this._client) throw new Error("No client");
+ return callback(this._client, task, io);
+ },
+ {
+ icon: "linear",
+ retry: retry.standardBackoff,
+ ...(options ?? {}),
+ connectionKey: this._connectionKey,
+ },
+ errorCallback ?? onError
+ );
+ }
+
+ async getAll<
+ TTask extends (
+ key: IntegrationTaskKey,
+ params: Partial>
+ ) => LinearReturnType>,
+ >(
+ task: TTask,
+ key: IntegrationTaskKey,
+ params: Nullable = {}
+ ): Promise>["nodes"]> {
+ const boundTask = task.bind(this);
+
+ let edges = await boundTask(`${key}-0`, params);
+ let nodes = edges.nodes;
+
+ for (let i = 1; edges.pageInfo.hasNextPage; i++) {
+ edges = await boundTask(`${key}-${i}`, { ...params, after: edges.pageInfo.endCursor });
+ nodes = nodes.concat(edges.nodes);
+ }
+
+ return nodes;
+ }
+
+ attachment(key: IntegrationTaskKey, params: { id: string }): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const entity = await client.attachment(params.id);
+ return serializeLinearOutput(entity);
+ },
+ {
+ name: "Get Attachment",
+ params,
+ properties: [{ label: "Attachment ID", text: params.id }],
+ }
+ );
+ }
+
+ attachments(
+ key: IntegrationTaskKey,
+ params: L.AttachmentsQueryVariables = {}
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const edges = await client.attachments(params);
+ return serializeLinearOutput(edges);
+ },
+ {
+ name: "Get Attachments",
+ params,
+ properties: queryProperties(params),
+ }
+ );
+ }
+
+ createAttachment(
+ key: IntegrationTaskKey,
+ params: L.AttachmentCreateInput
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.createAttachment(params);
+ return serializeLinearOutput(await payload.attachment);
+ },
+ {
+ name: "Create Attachment",
+ params,
+ properties: [
+ { label: "Issue ID", text: params.issueId },
+ { label: "Title", text: params.title },
+ { label: "URL", text: params.url },
+ ],
+ }
+ );
+ }
+
+ deleteAttachment(key: IntegrationTaskKey, params: { id: string }): Promise {
+ return this.runTask(key, (client) => client.deleteAttachment(params.id), {
+ name: "Delete Attachment",
+ params,
+ properties: [{ label: "Attachment ID", text: params.id }],
+ });
+ }
+
+ updateAttachment(
+ key: IntegrationTaskKey,
+ params: { id: string; input: L.AttachmentUpdateInput }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.updateAttachment(params.id, params.input);
+ return serializeLinearOutput(await payload.attachment);
+ },
+ {
+ name: "Update Attachment",
+ params,
+ properties: [{ label: "Attachment ID", text: params.id }],
+ }
+ );
+ }
+
+ attachmentLinkDiscord(
+ key: IntegrationTaskKey,
+ params: {
+ channelId: string;
+ issueId: string;
+ messageId: string;
+ url: string;
+ variables?: Omit<
+ L.AttachmentLinkDiscordMutationVariables,
+ "channelId" | "issueId" | "messageId" | "url"
+ >;
+ }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.attachmentLinkDiscord(
+ params.channelId,
+ params.issueId,
+ params.messageId,
+ params.url,
+ params.variables
+ );
+ return serializeLinearOutput(await payload.attachment);
+ },
+ {
+ name: "Link Discord Message",
+ params,
+ properties: [
+ { label: "Issue ID", text: params.issueId },
+ { label: "Channel ID", text: params.channelId },
+ { label: "Message ID", text: params.messageId },
+ { label: "URL", text: params.url },
+ ],
+ }
+ );
+ }
+
+ attachmentLinkFront(
+ key: IntegrationTaskKey,
+ params: {
+ conversationId: string;
+ issueId: string;
+ variables?: Omit;
+ }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.attachmentLinkFront(
+ params.conversationId,
+ params.issueId,
+ params.variables
+ );
+ return serializeLinearOutput(payload);
+ },
+ {
+ name: "Link Front Conversation",
+ params,
+ properties: [
+ { label: "Issue ID", text: params.issueId },
+ { label: "Conversation ID", text: params.conversationId },
+ ],
+ }
+ );
+ }
+
+ attachmentLinkIntercom(
+ key: IntegrationTaskKey,
+ params: {
+ conversationId: string;
+ issueId: string;
+ variables?: Omit;
+ }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.attachmentLinkIntercom(
+ params.conversationId,
+ params.issueId,
+ params.variables
+ );
+ return serializeLinearOutput(await payload.attachment);
+ },
+ {
+ name: "Link Intercom Conversation",
+ params,
+ properties: [
+ { label: "Issue ID", text: params.issueId },
+ { label: "Conversation ID", text: params.conversationId },
+ ],
+ }
+ );
+ }
+
+ attachmentLinkJiraIssue(
+ key: IntegrationTaskKey,
+ params: {
+ issueId: string;
+ jiraIssueId: string;
+ }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.attachmentLinkJiraIssue(params.issueId, params.jiraIssueId);
+ return serializeLinearOutput(await payload.attachment);
+ },
+ {
+ name: "Link Jira Issue",
+ params,
+ properties: [
+ { label: "Issue ID", text: params.issueId },
+ { label: "Jira Issue ID", text: params.jiraIssueId },
+ ],
+ }
+ );
+ }
+
+ attachmentLinkSlack(
+ key: IntegrationTaskKey,
+ params: {
+ channel: string;
+ issueId: string;
+ latest: string;
+ url: string;
+ variables?: Omit<
+ L.AttachmentLinkSlackMutationVariables,
+ "channel" | "issueId" | "latest" | "url"
+ >;
+ }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.attachmentLinkSlack(
+ params.channel,
+ params.issueId,
+ params.latest,
+ params.url,
+ params.variables
+ );
+ return serializeLinearOutput(await payload.attachment);
+ },
+ {
+ name: "Link Slack Message",
+ params,
+ properties: [
+ { label: "Issue ID", text: params.issueId },
+ { label: "Channel", text: params.channel },
+ { label: "Latest", text: params.latest },
+ { label: "URL", text: params.url },
+ ],
+ }
+ );
+ }
+
+ attachmentLinkURL(
+ key: IntegrationTaskKey,
+ params: {
+ issueId: string;
+ url: string;
+ variables?: Omit;
+ }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.attachmentLinkURL(
+ params.issueId,
+ params.url,
+ params.variables
+ );
+ return serializeLinearOutput(await payload.attachment);
+ },
+ {
+ name: "Link URL",
+ params,
+ properties: [
+ { label: "Issue ID", text: params.issueId },
+ { label: "URL", text: params.url },
+ ],
+ }
+ );
+ }
+
+ attachmentLinkZendesk(
+ key: IntegrationTaskKey,
+ params: {
+ issueId: string;
+ ticketId: string;
+ variables?: Omit;
+ }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.attachmentLinkZendesk(
+ params.issueId,
+ params.ticketId,
+ params.variables
+ );
+ return serializeLinearOutput(await payload.attachment);
+ },
+ {
+ name: "Link Zendesk Ticket",
+ params,
+ properties: [
+ { label: "Issue ID", text: params.issueId },
+ { label: "Ticket ID", text: params.ticketId },
+ ],
+ }
+ );
+ }
+
+ comment(key: IntegrationTaskKey, params: { id: string }): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const entity = await client.comment(params.id);
+ return serializeLinearOutput(entity);
+ },
+ {
+ name: "Get Comment",
+ params,
+ properties: [{ label: "Comment ID", text: params.id }],
+ }
+ );
+ }
+
+ comments(
+ key: IntegrationTaskKey,
+ params: L.CommentsQueryVariables = {}
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const edges = await client.comments(params);
+ return serializeLinearOutput(edges);
+ },
+ {
+ name: "Get Comments",
+ params,
+ properties: queryProperties(params),
+ }
+ );
+ }
+
+ createComment(
+ key: IntegrationTaskKey,
+ params: L.CommentCreateInput
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.createComment(params);
+ return serializeLinearOutput(await payload.comment);
+ },
+ {
+ name: "Create Comment",
+ params,
+ properties: [
+ { label: "Issue ID", text: params.issueId },
+ { label: "Body", text: params.body ?? "" },
+ ],
+ }
+ );
+ }
+
+ deleteComment(key: IntegrationTaskKey, params: { id: string }): Promise {
+ return this.runTask(key, (client) => client.deleteComment(params.id), {
+ name: "Delete Comment",
+ params,
+ properties: [{ label: "Comment ID", text: params.id }],
+ });
+ }
+
+ updateComment(
+ key: IntegrationTaskKey,
+ params: { id: string; input: L.CommentUpdateInput }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.updateComment(params.id, params.input);
+ return serializeLinearOutput(await payload.comment);
+ },
+ {
+ name: "Update Comment",
+ params,
+ properties: [{ label: "Comment ID", text: params.id }],
+ }
+ );
+ }
+
+ archiveCycle(
+ key: IntegrationTaskKey,
+ params: { id: string }
+ ): LinearReturnType> {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.archiveCycle(params.id);
+ return serializeLinearOutput({
+ ...payload,
+ entity: await payload.entity,
+ });
+ },
+ {
+ name: "Archive Cycle",
+ params,
+ properties: [{ label: "Cycle ID", text: params.id }],
+ }
+ );
+ }
+
+ createCycle(
+ key: IntegrationTaskKey,
+ params: L.CycleCreateInput
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.createCycle(params);
+ return serializeLinearOutput(await payload.cycle);
+ },
+ {
+ name: "Create Cycle",
+ params,
+ properties: [
+ { label: "Team ID", text: params.teamId },
+ { label: "Start at", text: params.startsAt.toISOString() },
+ { label: "Ends at", text: params.endsAt.toISOString() },
+ ],
+ }
+ );
+ }
+
+ // deleteCycle() does not exist
+
+ updateCycle(
+ key: IntegrationTaskKey,
+ params: { id: string; input: L.CycleUpdateInput }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.updateCycle(params.id, params.input);
+ return serializeLinearOutput(await payload.cycle);
+ },
+ {
+ name: "Update Cycle",
+ params,
+ properties: [{ label: "Cycle ID", text: params.id }],
+ }
+ );
+ }
+
+ document(key: IntegrationTaskKey, params: { id: string }): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const entity = await client.document(params.id);
+ return serializeLinearOutput(entity);
+ },
+ {
+ name: "Get Document",
+ params,
+ properties: [{ label: "Document ID", text: params.id }],
+ }
+ );
+ }
+
+ documents(
+ key: IntegrationTaskKey,
+ params: L.DocumentsQueryVariables
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const edges = await client.documents(params);
+ return serializeLinearOutput(edges);
+ },
+ {
+ name: "Get Documents",
+ params,
+ properties: queryProperties(params),
+ }
+ );
+ }
+
+ createDocument(
+ key: IntegrationTaskKey,
+ params: L.DocumentCreateInput
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.createDocument(params);
+ return serializeLinearOutput(await payload.document);
+ },
+ {
+ name: "Create Document",
+ params,
+ properties: [
+ { label: "Project ID", text: params.projectId },
+ { label: "Title", text: params.title },
+ ],
+ }
+ );
+ }
+
+ searchDocuments(
+ key: IntegrationTaskKey,
+ params: {
+ term: string;
+ variables?: L.SearchDocumentsQueryVariables;
+ }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.searchDocuments(params.term, params.variables);
+ return serializeLinearOutput(payload);
+ },
+ {
+ name: "Search Documents",
+ params,
+ properties: [{ label: "Search Term", text: params.term }],
+ }
+ );
+ }
+
+ favorite(key: IntegrationTaskKey, params: { id: string }): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const entity = await client.favorite(params.id);
+ return serializeLinearOutput(entity);
+ },
+ {
+ name: "Get Favorite",
+ params,
+ properties: [{ label: "Favorite ID", text: params.id }],
+ }
+ );
+ }
+
+ favorites(
+ key: IntegrationTaskKey,
+ params: L.FavoritesQueryVariables = {}
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const edges = await client.favorites(params);
+ return serializeLinearOutput(edges);
+ },
+ {
+ name: "Get Favorites",
+ params,
+ properties: queryProperties(params),
+ }
+ );
+ }
+
+ createFavorite(
+ key: IntegrationTaskKey,
+ params: L.FavoriteCreateInput
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.createFavorite(params);
+ return serializeLinearOutput(await payload.favorite);
+ },
+ {
+ name: "Create Favorite",
+ params,
+ }
+ );
+ }
+
+ issue(key: IntegrationTaskKey, params: { id: string }): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const entity = await client.issue(params.id);
+ return serializeLinearOutput(entity);
+ },
+ {
+ name: "Get Issue",
+ params,
+ properties: [{ label: "Issue ID", text: params.id }],
+ }
+ );
+ }
+
+ issues(
+ key: IntegrationTaskKey,
+ params: L.IssuesQueryVariables = {}
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const edges = await client.issues(params);
+ return serializeLinearOutput(edges);
+ },
+ {
+ name: "Get Issues",
+ params,
+ properties: queryProperties(params),
+ }
+ );
+ }
+
+ archiveIssue(
+ key: IntegrationTaskKey,
+ params: {
+ id: string;
+ variables?: Omit;
+ }
+ ): LinearReturnType> {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.archiveIssue(params.id, params.variables);
+ return serializeLinearOutput({
+ ...payload,
+ entity: await payload.entity,
+ });
+ },
+ {
+ name: "Archive Issue",
+ params,
+ properties: [{ label: "Issue ID", text: params.id }],
+ }
+ );
+ }
+
+ createIssue(
+ key: IntegrationTaskKey,
+ params: L.IssueCreateInput & { title: string }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.createIssue(params);
+ return serializeLinearOutput(await payload.issue);
+ },
+ {
+ name: "Create Issue",
+ params,
+ properties: [
+ { label: "Team ID", text: params.teamId },
+ { label: "Title", text: params.title },
+ ],
+ }
+ );
+ }
+
+ deleteIssue(
+ key: IntegrationTaskKey,
+ params: { id: string }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.deleteIssue(params.id);
+ return serializeLinearOutput(await payload.entity);
+ },
+ {
+ name: "Delete Issue",
+ params,
+ properties: [{ label: "Issue ID", text: params.id }],
+ }
+ );
+ }
+
+ searchIssues(
+ key: IntegrationTaskKey,
+ params: {
+ term: string;
+ variables?: L.SearchIssuesQueryVariables;
+ }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.searchIssues(params.term, params.variables);
+ return serializeLinearOutput(payload);
+ },
+ {
+ name: "Search Issues",
+ params,
+ properties: [{ label: "Search Term", text: params.term }],
+ }
+ );
+ }
+
+ updateIssue(
+ key: IntegrationTaskKey,
+ params: { id: string; input: L.IssueUpdateInput }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.updateIssue(params.id, params.input);
+ return serializeLinearOutput(await payload.issue);
+ },
+ {
+ name: "Update Issue",
+ params,
+ properties: [{ label: "Issue ID", text: params.id }],
+ }
+ );
+ }
+
+ issueLabel(key: IntegrationTaskKey, params: { id: string }): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const entity = await client.issueLabel(params.id);
+ return serializeLinearOutput(entity);
+ },
+ {
+ name: "Get IssueLabel",
+ params,
+ properties: [{ label: "IssueLabel ID", text: params.id }],
+ }
+ );
+ }
+
+ issueLabels(
+ key: IntegrationTaskKey,
+ params: L.IssueLabelsQueryVariables = {}
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const edges = await client.issueLabels(params);
+ return serializeLinearOutput(edges);
+ },
+ {
+ name: "Get IssueLabels",
+ params,
+ properties: queryProperties(params),
+ }
+ );
+ }
+
+ createIssueLabel(
+ key: IntegrationTaskKey,
+ params: L.IssueLabelCreateInput
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.createIssueLabel(params);
+ return serializeLinearOutput(await payload.issueLabel);
+ },
+ {
+ name: "Create IssueLabel",
+ params,
+ properties: [{ label: "Label name", text: params.name }],
+ }
+ );
+ }
+
+ deleteIssueLabel(key: IntegrationTaskKey, params: { id: string }): Promise {
+ return this.runTask(key, (client) => client.deleteIssueLabel(params.id), {
+ name: "Delete IssueLabel",
+ params,
+ properties: [{ label: "Label ID", text: params.id }],
+ });
+ }
+
+ updateIssueLabel(
+ key: IntegrationTaskKey,
+ params: { id: string; input: L.IssueLabelUpdateInput }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.updateIssueLabel(params.id, params.input);
+ return serializeLinearOutput(await payload.issueLabel);
+ },
+ {
+ name: "Update IssueLabel",
+ params,
+ properties: [{ label: "Label ID", text: params.id }],
+ }
+ );
+ }
+
+ issuePriorityValues(key: IntegrationTaskKey): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const entity = await client.issuePriorityValues;
+ return serializeLinearOutput(entity);
+ },
+ {
+ name: "Get Issue Priority Values",
+ }
+ );
+ }
+
+ issueRelation(key: IntegrationTaskKey, params: { id: string }): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const entity = await client.issueRelation(params.id);
+ return serializeLinearOutput(entity);
+ },
+ {
+ name: "Get IssueRelation",
+ params,
+ properties: [{ label: "IssueRelation ID", text: params.id }],
+ }
+ );
+ }
+
+ issueRelations(
+ key: IntegrationTaskKey,
+ params: L.IssueRelationsQueryVariables = {}
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const edges = await client.issueRelations(params);
+ return serializeLinearOutput(edges);
+ },
+ {
+ name: "Get IssueRelations",
+ params,
+ properties: queryProperties(params),
+ }
+ );
+ }
+
+ createIssueRelation(
+ key: IntegrationTaskKey,
+ params: L.IssueRelationCreateInput
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.createIssueRelation(params);
+ return serializeLinearOutput(await payload.issueRelation);
+ },
+ {
+ name: "Create IssueRelation",
+ params,
+ properties: [
+ { label: "Issue ID", text: params.issueId },
+ { label: "Related Issue ID", text: params.relatedIssueId },
+ { label: "Relation Type", text: params.type },
+ ],
+ }
+ );
+ }
+
+ notification(key: IntegrationTaskKey, params: { id: string }): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const entity = await client.notification(params.id);
+ return serializeLinearOutput(entity);
+ },
+ {
+ name: "Get Notification",
+ params,
+ properties: [{ label: "Notification ID", text: params.id }],
+ }
+ );
+ }
+
+ notifications(
+ key: IntegrationTaskKey,
+ params: L.NotificationsQueryVariables = {}
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const edges = await client.notifications(params);
+ return serializeLinearOutput(edges);
+ },
+ {
+ name: "Get Notifications",
+ params,
+ properties: queryProperties(params),
+ }
+ );
+ }
+
+ archiveNotification(
+ key: IntegrationTaskKey,
+ params: { id: string }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.archiveNotification(params.id);
+ return serializeLinearOutput(payload);
+ },
+ {
+ name: "Archive Notification",
+ params,
+ properties: [{ label: "Notification ID", text: params.id }],
+ }
+ );
+ }
+
+ createNotificationSubscription(
+ key: IntegrationTaskKey,
+ params: {
+ input: L.NotificationSubscriptionCreateInput;
+ }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.createNotificationSubscription(params.input);
+ return serializeLinearOutput(payload);
+ },
+ {
+ name: "Create Notification Subscription",
+ params,
+ }
+ );
+ }
+
+ organization(key: IntegrationTaskKey): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const entity = await client.organization;
+ return serializeLinearOutput(entity);
+ },
+ {
+ name: "Get Viewer's Organization",
+ }
+ );
+ }
+
+ createOrganizationFromOnboarding(
+ key: IntegrationTaskKey,
+ params: {
+ input: L.CreateOrganizationInput;
+ variables?: Omit;
+ }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.createOrganizationFromOnboarding(
+ params.input,
+ params.variables
+ );
+ return serializeLinearOutput(await payload.organization);
+ },
+ {
+ name: "Create Organization",
+ params,
+ properties: [
+ { label: "Name", text: params.input.name },
+ { label: "URL Key", text: params.input.urlKey },
+ ],
+ }
+ );
+ }
+
+ /** WARNING: Causes internal server errors on Linear's side, regardless of input. */
+ createOrganizationInvite(
+ key: IntegrationTaskKey,
+ params: {
+ input: L.OrganizationInviteCreateInput;
+ }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.createOrganizationInvite(params.input);
+ return serializeLinearOutput(await payload.organizationInvite);
+ },
+ {
+ name: "Create Organization Invite",
+ params,
+ properties: [{ label: "Invitee Email", text: params.input.email }],
+ }
+ );
+ }
+
+ project(key: IntegrationTaskKey, params: { id: string }): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const entity = await client.project(params.id);
+ return serializeLinearOutput(entity);
+ },
+ {
+ name: "Get Project",
+ params,
+ properties: [{ label: "Project ID", text: params.id }],
+ }
+ );
+ }
+
+ projects(
+ key: IntegrationTaskKey,
+ params: L.ProjectsQueryVariables = {}
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const edges = await client.projects(params);
+ return serializeLinearOutput(edges);
+ },
+ {
+ name: "Get Projects",
+ params,
+ properties: queryProperties(params),
+ }
+ );
+ }
+
+ archiveProject(
+ key: IntegrationTaskKey,
+ params: {
+ id: string;
+ variables?: Omit;
+ }
+ ): LinearReturnType> {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.archiveProject(params.id, params.variables);
+ return serializeLinearOutput({
+ ...payload,
+ entity: await payload.entity,
+ });
+ },
+ {
+ name: "Archive Project",
+ params,
+ properties: [{ label: "Project ID", text: params.id }],
+ }
+ );
+ }
+
+ createProject(
+ key: IntegrationTaskKey,
+ params: L.ProjectCreateInput
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.createProject(params);
+ return serializeLinearOutput(await payload.project);
+ },
+ {
+ name: "Create Project",
+ params,
+ properties: [
+ { label: "Team IDs", text: params.teamIds.join(", ") },
+ { label: "Project name", text: params.name },
+ ],
+ }
+ );
+ }
+
+ deleteProject(
+ key: IntegrationTaskKey,
+ params: { id: string }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.deleteProject(params.id);
+ return serializeLinearOutput(await payload.entity);
+ },
+ {
+ name: "Delete Project",
+ params,
+ properties: [{ label: "Project ID", text: params.id }],
+ }
+ );
+ }
+
+ searchProjects(
+ key: IntegrationTaskKey,
+ params: {
+ term: string;
+ variables?: L.SearchProjectsQueryVariables;
+ }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.searchProjects(params.term, params.variables);
+ return serializeLinearOutput(payload);
+ },
+ {
+ name: "Search Projects",
+ params,
+ properties: [{ label: "Search Term", text: params.term }],
+ }
+ );
+ }
+
+ updateProject(
+ key: IntegrationTaskKey,
+ params: { id: string; input: L.ProjectUpdateInput }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.updateProject(params.id, params.input);
+ return serializeLinearOutput(await payload.project);
+ },
+ {
+ name: "Update Project",
+ params,
+ properties: [{ label: "Project ID", text: params.id }],
+ }
+ );
+ }
+
+ projectLink(key: IntegrationTaskKey, params: { id: string }): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const entity = await client.projectLink(params.id);
+ return serializeLinearOutput(entity);
+ },
+ {
+ name: "Get ProjectLink",
+ params,
+ properties: [{ label: "ProjectLink ID", text: params.id }],
+ }
+ );
+ }
+
+ projectLinks(
+ key: IntegrationTaskKey,
+ params: L.ProjectLinksQueryVariables = {}
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const edges = await client.projectLinks(params);
+ return serializeLinearOutput(edges);
+ },
+ {
+ name: "Get ProjectLinks",
+ params,
+ properties: queryProperties(params),
+ }
+ );
+ }
+
+ createProjectLink(
+ key: IntegrationTaskKey,
+ params: L.ProjectLinkCreateInput
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.createProjectLink(params);
+ return serializeLinearOutput(await payload.projectLink);
+ },
+ {
+ name: "Create ProjectLink",
+ params,
+ properties: [
+ { label: "Project ID", text: params.projectId },
+ { label: "Link Label", text: params.label },
+ { label: "Link URL", text: params.url },
+ ],
+ }
+ );
+ }
+
+ createProjectMilestone(
+ key: IntegrationTaskKey,
+ params: L.ProjectMilestoneCreateInput
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.createProjectMilestone(params);
+ return serializeLinearOutput(await payload.projectMilestone);
+ },
+ {
+ name: "Create ProjectMilestone",
+ params,
+ properties: [
+ { label: "Project ID", text: params.projectId },
+ { label: "Milestone Name", text: params.name },
+ ],
+ }
+ );
+ }
+
+ projectUpdate(key: IntegrationTaskKey, params: { id: string }): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const entity = await client.projectUpdate(params.id);
+ return serializeLinearOutput(entity);
+ },
+ {
+ name: "Get ProjectUpdate",
+ params,
+ properties: [{ label: "ProjectUpdate ID", text: params.id }],
+ }
+ );
+ }
+
+ projectUpdates(
+ key: IntegrationTaskKey,
+ params: L.ProjectUpdatesQueryVariables = {}
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const edges = await client.projectUpdates(params);
+ return serializeLinearOutput(edges);
+ },
+ {
+ name: "Get ProjectUpdates",
+ params,
+ properties: queryProperties(params),
+ }
+ );
+ }
+
+ createProjectUpdate(
+ key: IntegrationTaskKey,
+ params: L.ProjectUpdateCreateInput
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.createProjectUpdate(params);
+ return serializeLinearOutput(await payload.projectUpdate);
+ },
+ {
+ name: "Create ProjectUpdate",
+ params,
+ properties: [{ label: "Project ID", text: params.projectId }],
+ }
+ );
+ }
+
+ deleteProjectUpdate(key: IntegrationTaskKey, params: { id: string }): Promise {
+ return this.runTask(key, (client) => client.deleteProjectUpdate(params.id), {
+ name: "Delete ProjectUpdate",
+ params,
+ properties: [{ label: "ProjectUpdate ID", text: params.id }],
+ });
+ }
+
+ updateProjectUpdate(
+ key: IntegrationTaskKey,
+ params: { id: string; input: L.ProjectUpdateUpdateInput }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.updateProjectUpdate(params.id, params.input);
+ return serializeLinearOutput(await payload.projectUpdate);
+ },
+ {
+ name: "Update ProjectUpdate",
+ params,
+ properties: [{ label: "ProjectUpdate ID", text: params.id }],
+ }
+ );
+ }
+
+ createReaction(
+ key: IntegrationTaskKey,
+ params: L.ReactionCreateInput & { emoji: string }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.createReaction(params);
+ return serializeLinearOutput(await payload.reaction);
+ },
+ {
+ name: "Create Reaction",
+ params,
+ properties: [
+ ...(params.commentId ? [{ label: "Comment ID", text: params.commentId }] : []),
+ ...(params.issueId ? [{ label: "Issue ID", text: params.issueId }] : []),
+ ...(params.projectUpdateId
+ ? [{ label: "ProjectUpdate ID", text: params.projectUpdateId }]
+ : []),
+ { label: "Emoji", text: params.emoji },
+ ],
+ }
+ );
+ }
+
+ deleteReaction(key: IntegrationTaskKey, params: { id: string }): Promise {
+ return this.runTask(key, (client) => client.deleteReaction(params.id), {
+ name: "Delete Reaction",
+ params,
+ properties: [{ label: "Reaction ID", text: params.id }],
+ });
+ }
+
+ archiveRoadmap(
+ key: IntegrationTaskKey,
+ params: { id: string }
+ ): LinearReturnType> {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.archiveRoadmap(params.id);
+ return serializeLinearOutput({
+ ...payload,
+ entity: await payload.entity,
+ });
+ },
+ {
+ name: "Archive Roadmap",
+ params,
+ properties: [{ label: "Roadmap ID", text: params.id }],
+ }
+ );
+ }
+
+ createRoadmap(
+ key: IntegrationTaskKey,
+ params: L.RoadmapCreateInput
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.createRoadmap(params);
+ return serializeLinearOutput(await payload.roadmap);
+ },
+ {
+ name: "Create Roadmap",
+ params,
+ properties: [{ label: "Roadmap Name", text: params.name }],
+ }
+ );
+ }
+
+ team(key: IntegrationTaskKey, params: { id: string }): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const entity = await client.team(params.id);
+ return serializeLinearOutput(entity);
+ },
+ {
+ name: "Get Team",
+ params,
+ properties: [{ label: "Team ID", text: params.id }],
+ }
+ );
+ }
+
+ teams(
+ key: IntegrationTaskKey,
+ params: L.TeamsQueryVariables = {}
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const edges = await client.teams(params);
+ return serializeLinearOutput(edges);
+ },
+ {
+ name: "Get Teams",
+ params,
+ properties: queryProperties(params),
+ }
+ );
+ }
+
+ createTeam(
+ key: IntegrationTaskKey,
+ params: L.TeamCreateInput
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.createTeam(params);
+ return serializeLinearOutput(await payload.team);
+ },
+ {
+ name: "Create Team",
+ params,
+ properties: [{ label: "Team Name", text: params.name }],
+ }
+ );
+ }
+
+ teamMembership(
+ key: IntegrationTaskKey,
+ params: { id: string }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const entity = await client.teamMembership(params.id);
+ return serializeLinearOutput(entity);
+ },
+ {
+ name: "Get TeamMembership",
+ params,
+ properties: [{ label: "TeamMembership ID", text: params.id }],
+ }
+ );
+ }
+
+ teamMemberships(
+ key: IntegrationTaskKey,
+ params: L.TeamMembershipsQueryVariables = {}
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const edges = await client.teamMemberships(params);
+ return serializeLinearOutput(edges);
+ },
+ {
+ name: "Get TeamMemberships",
+ params,
+ properties: queryProperties(params),
+ }
+ );
+ }
+
+ createTeamMembership(
+ key: IntegrationTaskKey,
+ params: L.TeamMembershipCreateInput
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.createTeamMembership(params);
+ return serializeLinearOutput(await payload.teamMembership);
+ },
+ {
+ name: "Create TeamMembership",
+ params,
+ properties: [
+ { label: "Team ID", text: params.teamId },
+ { label: "User ID", text: params.userId },
+ ],
+ }
+ );
+ }
+
+ template(key: IntegrationTaskKey, params: { id: string }): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const entity = await client.template(params.id);
+ return serializeLinearOutput(entity);
+ },
+ {
+ name: "Get Template",
+ properties: [{ label: "Template ID", text: params.id }],
+ }
+ );
+ }
+
+ templates(key: IntegrationTaskKey): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const entity = await client.templates;
+ return serializeLinearOutput(entity);
+ },
+ {
+ name: "Get Templates",
+ }
+ );
+ }
+
+ user(key: IntegrationTaskKey, params: { id: string }): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const entity = await client.user(params.id);
+ return serializeLinearOutput(entity);
+ },
+ {
+ name: "Get User",
+ params,
+ properties: [{ label: "User ID", text: params.id }],
+ }
+ );
+ }
+
+ users(
+ key: IntegrationTaskKey,
+ params: L.UsersQueryVariables = {}
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const edges = await client.users(params);
+ return serializeLinearOutput(edges);
+ },
+ {
+ name: "Get Users",
+ params,
+ properties: queryProperties(params),
+ }
+ );
+ }
+
+ updateUser(
+ key: IntegrationTaskKey,
+ params: { id: string; input: L.UpdateUserInput }
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.updateUser(params.id, params.input);
+ return serializeLinearOutput(await payload.user);
+ },
+ {
+ name: "Update User",
+ params,
+ properties: [{ label: "User ID", text: params.id }],
+ }
+ );
+ }
+
+ viewer(key: IntegrationTaskKey): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const entity = await client.viewer;
+ return serializeLinearOutput(entity);
+ },
+ {
+ name: "Get Viewer",
+ }
+ );
+ }
+
+ workflowState(key: IntegrationTaskKey, params: { id: string }): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const entity = await client.workflowState(params.id);
+ return serializeLinearOutput(entity);
+ },
+ {
+ name: "Get WorkflowState",
+ params,
+ properties: [{ label: "WorkflowState ID", text: params.id }],
+ }
+ );
+ }
+
+ workflowStates(
+ key: IntegrationTaskKey,
+ params: L.WorkflowStatesQueryVariables = {}
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const edges = await client.workflowStates(params);
+ return serializeLinearOutput(edges);
+ },
+ {
+ name: "Get WorkflowStates",
+ params,
+ properties: queryProperties(params),
+ }
+ );
+ }
+
+ archiveWorkflowState(
+ key: IntegrationTaskKey,
+ params: { id: string }
+ ): LinearReturnType> {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.archiveWorkflowState(params.id);
+ return serializeLinearOutput({
+ ...payload,
+ entity: await payload.entity,
+ });
+ },
+ {
+ name: "Archive WorkflowState",
+ params,
+ properties: [{ label: "WorkflowState ID", text: params.id }],
+ }
+ );
+ }
+
+ createWorkflowState(
+ key: IntegrationTaskKey,
+ params: L.WorkflowStateCreateInput
+ ): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client) => {
+ const payload = await client.createWorkflowState(params);
+ return serializeLinearOutput(await payload.workflowState);
+ },
+ {
+ name: "Create WorkflowState",
+ params,
+ properties: [
+ { label: "Team ID", text: params.teamId },
+ { label: "Workflow Type", text: params.type },
+ { label: "State Name", text: params.name },
+ { label: "State Color", text: params.color },
+ ],
+ }
+ );
+ }
+
+ // updateReaction() does not exist
+
+ /** **WARNING:** Still in alpha - use with caution! */
+ onAttachment(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onAttachment, params);
+ }
+
+ /** **WARNING:** Still in alpha - use with caution! */
+ onAttachmentCreated(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onAttachmentCreated, params);
+ }
+
+ /** **WARNING:** Still in alpha - use with caution! */
+ onAttachmentRemoved(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onAttachmentRemoved, params);
+ }
+
+ /** **WARNING:** Still in alpha - use with caution! */
+ onAttachmentUpdated(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onAttachmentUpdated, params);
+ }
+
+ onComment(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onComment, params);
+ }
+
+ onCommentCreated(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onCommentCreated, params);
+ }
+
+ onCommentRemoved(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onCommentRemoved, params);
+ }
+
+ onCommentUpdated(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onCommentUpdated, params);
+ }
+
+ onCycle(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onCycle, params);
+ }
+
+ onCycleCreated(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onCycleCreated, params);
+ }
+
+ onCycleRemoved(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onCycleRemoved, params);
+ }
+
+ onCycleUpdated(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onCycleUpdated, params);
+ }
+
+ onIssue(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onIssue, params);
+ }
+
+ onIssueCreated(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onIssueCreated, params);
+ }
+
+ onIssueRemoved(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onIssueRemoved, params);
+ }
+
+ onIssueUpdated(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onIssueUpdated, params);
+ }
+
+ onIssueLabel(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onIssueLabel, params);
+ }
+
+ onIssueLabelCreated(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onIssueLabelCreated, params);
+ }
+
+ onIssueLabelRemoved(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onIssueLabelRemoved, params);
+ }
+
+ onIssueLabelUpdated(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onIssueLabelUpdated, params);
+ }
+
+ onIssueSLA(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onIssueSLA, params);
+ }
+
+ onIssueSLASet(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onIssueSLASet, params);
+ }
+
+ onIssueSLABreached(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onIssueSLABreached, params);
+ }
+
+ onIssueSLAHighRisk(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onIssueSLAHighRisk, params);
+ }
+
+ onProject(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onProject, params);
+ }
+
+ onProjectCreated(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onProjectCreated, params);
+ }
+
+ onProjectRemoved(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onProjectRemoved, params);
+ }
+
+ onProjectUpdated(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onProjectUpdated, params);
+ }
+
+ onProjectUpdate(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onProjectUpdate, params);
+ }
+
+ onProjectUpdateCreated(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onProjectUpdateCreated, params);
+ }
+
+ onProjectUpdateRemoved(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onProjectUpdateRemoved, params);
+ }
+
+ onProjectUpdateUpdated(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onProjectUpdateUpdated, params);
+ }
+
+ onReaction(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onReaction, params);
+ }
+
+ onReactionCreated(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onReactionCreated, params);
+ }
+
+ onReactionRemoved(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onReactionRemoved, params);
+ }
+
+ /** Good luck ever triggering this! */
+ onReactionUpdated(params: TriggerParams = {}) {
+ return createTrigger(this.source, events.onReactionUpdated, params);
+ }
+
+ get #webhooks() {
+ return new Webhooks(this.runTask.bind(this));
+ }
+
+ webhook = this.#webhooks.webhook;
+ webhooks = this.#webhooks.webhooks;
+
+ createWebhook = this.#webhooks.createWebhook;
+ deleteWebhook = this.#webhooks.deleteWebhook;
+ updateWebhook = this.#webhooks.updateWebhook;
+}
+
+export function onError(error: unknown): ReturnType {
+ if (error instanceof LinearError) {
+ // fail fast on user errors
+ if (error.errors?.some((e) => e.userError)) {
+ return {
+ skipRetrying: true,
+ };
+ }
+ }
+
+ if (!(error instanceof RatelimitedLinearError)) {
+ return;
+ }
+
+ const rateLimitRemaining = error.raw?.response?.headers?.get("X-RateLimit-Requests-Remaining");
+ const rateLimitReset = error.raw?.response?.headers?.get("X-RateLimit-Requests-Reset");
+
+ if (rateLimitRemaining === "0" && rateLimitReset) {
+ const resetDate = new Date(Number(rateLimitReset) * 1000);
+
+ return {
+ retryAt: resetDate,
+ error,
+ };
+ }
+
+ const queryComplexity = error.raw?.response?.headers?.get("X-Complexity");
+ const complexityRemaining = error.raw?.response?.headers?.get("X-RateLimit-Complexity-Remaining");
+ const complexityReset = error.raw?.response?.headers?.get("X-RateLimit-Complexity-Reset");
+
+ if (
+ (complexityRemaining === "0" || Number(complexityRemaining) < Number(queryComplexity)) &&
+ complexityReset
+ ) {
+ const resetDate = new Date(Number(complexityReset) * 1000);
+
+ return {
+ retryAt: resetDate,
+ error,
+ };
+ }
+}
+
+export const serializeLinearOutput = (obj: T): Prettify> => {
+ return JSON.parse(JSON.stringify(obj), (key, value) => {
+ if (typeof value === "function" || key.startsWith("_")) {
+ return undefined;
+ }
+ return value;
+ });
+};
+
+export { events };
+
+export const PaginationOrderBy = L.PaginationOrderBy
diff --git a/integrations/linear/src/payload-examples/Attachment.json b/integrations/linear/src/payload-examples/Attachment.json
new file mode 100644
index 00000000000..b685ac7ecbc
--- /dev/null
+++ b/integrations/linear/src/payload-examples/Attachment.json
@@ -0,0 +1,25 @@
+{
+ "data": {
+ "id": "8a4c109b-3288-4bef-8a80-8c1618e0f4ad",
+ "url": "https://www.rickastley.co.uk/",
+ "title": "My Personal Website",
+ "source": {
+ "type": "api",
+ "imageUrl": "https://uploads.linear.app/attachment-icons/6fca65e5bca9d4ac8eb9d9442c5e3170081f0aa3eb820eebd58766b53fe7236c"
+ },
+ "issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3",
+ "metadata": {},
+ "subtitle": "Rick Astley Official Site",
+ "createdAt": "2023-09-17T10:51:34.932Z",
+ "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "updatedAt": "2023-09-17T10:51:34.932Z",
+ "sourceType": "api",
+ "groupBySource": false
+ },
+ "type": "Attachment",
+ "action": "create",
+ "createdAt": "2023-09-17T10:51:34.932Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T10:51:37.795Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/AttachmentCreated.json b/integrations/linear/src/payload-examples/AttachmentCreated.json
new file mode 100644
index 00000000000..b685ac7ecbc
--- /dev/null
+++ b/integrations/linear/src/payload-examples/AttachmentCreated.json
@@ -0,0 +1,25 @@
+{
+ "data": {
+ "id": "8a4c109b-3288-4bef-8a80-8c1618e0f4ad",
+ "url": "https://www.rickastley.co.uk/",
+ "title": "My Personal Website",
+ "source": {
+ "type": "api",
+ "imageUrl": "https://uploads.linear.app/attachment-icons/6fca65e5bca9d4ac8eb9d9442c5e3170081f0aa3eb820eebd58766b53fe7236c"
+ },
+ "issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3",
+ "metadata": {},
+ "subtitle": "Rick Astley Official Site",
+ "createdAt": "2023-09-17T10:51:34.932Z",
+ "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "updatedAt": "2023-09-17T10:51:34.932Z",
+ "sourceType": "api",
+ "groupBySource": false
+ },
+ "type": "Attachment",
+ "action": "create",
+ "createdAt": "2023-09-17T10:51:34.932Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T10:51:37.795Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/AttachmentRemoved.json b/integrations/linear/src/payload-examples/AttachmentRemoved.json
new file mode 100644
index 00000000000..d7d0f713b00
--- /dev/null
+++ b/integrations/linear/src/payload-examples/AttachmentRemoved.json
@@ -0,0 +1,25 @@
+{
+ "data": {
+ "id": "8a4c109b-3288-4bef-8a80-8c1618e0f4ad",
+ "url": "https://www.rickastley.co.uk/",
+ "title": "Will Never Give You Up",
+ "source": {
+ "type": "api",
+ "imageUrl": "https://uploads.linear.app/attachment-icons/6fca65e5bca9d4ac8eb9d9442c5e3170081f0aa3eb820eebd58766b53fe7236c"
+ },
+ "issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3",
+ "metadata": {},
+ "subtitle": "Rick Astley Official Site",
+ "createdAt": "2023-09-17T10:51:34.932Z",
+ "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "updatedAt": "2023-09-17T10:51:55.751Z",
+ "sourceType": "api",
+ "groupBySource": false
+ },
+ "type": "Attachment",
+ "action": "remove",
+ "createdAt": "2023-09-17T10:52:15.457Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T10:52:15.938Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/AttachmentUpdated.json b/integrations/linear/src/payload-examples/AttachmentUpdated.json
new file mode 100644
index 00000000000..95d247b48b6
--- /dev/null
+++ b/integrations/linear/src/payload-examples/AttachmentUpdated.json
@@ -0,0 +1,29 @@
+{
+ "data": {
+ "id": "8a4c109b-3288-4bef-8a80-8c1618e0f4ad",
+ "url": "https://www.rickastley.co.uk/",
+ "title": "Will Never Give You Up",
+ "source": {
+ "type": "api",
+ "imageUrl": "https://uploads.linear.app/attachment-icons/6fca65e5bca9d4ac8eb9d9442c5e3170081f0aa3eb820eebd58766b53fe7236c"
+ },
+ "issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3",
+ "metadata": {},
+ "subtitle": "Rick Astley Official Site",
+ "createdAt": "2023-09-17T10:51:34.932Z",
+ "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "updatedAt": "2023-09-17T10:51:55.751Z",
+ "sourceType": "api",
+ "groupBySource": false
+ },
+ "type": "Attachment",
+ "action": "update",
+ "createdAt": "2023-09-17T10:51:55.751Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "updatedFrom": {
+ "title": "My Personal Website",
+ "updatedAt": "2023-09-17T10:51:34.932Z"
+ },
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T10:51:55.809Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/Comment.json b/integrations/linear/src/payload-examples/Comment.json
new file mode 100644
index 00000000000..a8f0f128d70
--- /dev/null
+++ b/integrations/linear/src/payload-examples/Comment.json
@@ -0,0 +1,22 @@
+{
+ "url": "https://linear.app/triggerdotdev/issue/TRI-1/welcome-to-linear-👋#comment-a4083fcd",
+ "data": {
+ "id": "a4083fcd-2513-4a9b-9a73-bab21cd56a95",
+ "body": "I just wanna tell you how I'm feeling",
+ "issue": {
+ "id": "ee4d5612-0752-441a-95cd-2a0de56689d3",
+ "title": "Welcome to Rick's World ❤️🔥"
+ },
+ "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3",
+ "createdAt": "2023-09-17T10:57:11.751Z",
+ "updatedAt": "2023-09-17T10:57:11.751Z",
+ "reactionData": []
+ },
+ "type": "Comment",
+ "action": "create",
+ "createdAt": "2023-09-17T10:57:11.751Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T10:57:11.842Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/CommentCreated.json b/integrations/linear/src/payload-examples/CommentCreated.json
new file mode 100644
index 00000000000..a8f0f128d70
--- /dev/null
+++ b/integrations/linear/src/payload-examples/CommentCreated.json
@@ -0,0 +1,22 @@
+{
+ "url": "https://linear.app/triggerdotdev/issue/TRI-1/welcome-to-linear-👋#comment-a4083fcd",
+ "data": {
+ "id": "a4083fcd-2513-4a9b-9a73-bab21cd56a95",
+ "body": "I just wanna tell you how I'm feeling",
+ "issue": {
+ "id": "ee4d5612-0752-441a-95cd-2a0de56689d3",
+ "title": "Welcome to Rick's World ❤️🔥"
+ },
+ "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3",
+ "createdAt": "2023-09-17T10:57:11.751Z",
+ "updatedAt": "2023-09-17T10:57:11.751Z",
+ "reactionData": []
+ },
+ "type": "Comment",
+ "action": "create",
+ "createdAt": "2023-09-17T10:57:11.751Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T10:57:11.842Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/CommentRemoved.json b/integrations/linear/src/payload-examples/CommentRemoved.json
new file mode 100644
index 00000000000..eadef86b5dc
--- /dev/null
+++ b/integrations/linear/src/payload-examples/CommentRemoved.json
@@ -0,0 +1,22 @@
+{
+ "data": {
+ "id": "a4083fcd-2513-4a9b-9a73-bab21cd56a95",
+ "body": "Gotta make you understand",
+ "issue": {
+ "id": "ee4d5612-0752-441a-95cd-2a0de56689d3",
+ "title": "Welcome to Rick's World ❤️🔥"
+ },
+ "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3",
+ "editedAt": "2023-09-17T10:57:24.386Z",
+ "createdAt": "2023-09-17T10:57:11.751Z",
+ "updatedAt": "2023-09-17T10:57:24.387Z",
+ "reactionData": []
+ },
+ "type": "Comment",
+ "action": "remove",
+ "createdAt": "2023-09-17T10:57:37.324Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T10:57:37.376Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/CommentUpdated.json b/integrations/linear/src/payload-examples/CommentUpdated.json
new file mode 100644
index 00000000000..6b930298188
--- /dev/null
+++ b/integrations/linear/src/payload-examples/CommentUpdated.json
@@ -0,0 +1,28 @@
+{
+ "url": "https://linear.app/triggerdotdev/issue/TRI-1/welcome-to-linear-👋#comment-a4083fcd",
+ "data": {
+ "id": "a4083fcd-2513-4a9b-9a73-bab21cd56a95",
+ "body": "Gotta make you understand",
+ "issue": {
+ "id": "ee4d5612-0752-441a-95cd-2a0de56689d3",
+ "title": "Welcome to Rick's World ❤️🔥"
+ },
+ "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3",
+ "editedAt": "2023-09-17T10:57:24.386Z",
+ "createdAt": "2023-09-17T10:57:11.751Z",
+ "updatedAt": "2023-09-17T10:57:24.387Z",
+ "reactionData": []
+ },
+ "type": "Comment",
+ "action": "update",
+ "createdAt": "2023-09-17T10:57:24.387Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "updatedFrom": {
+ "body": "I just wanna tell you how I'm feeling",
+ "editedAt": null,
+ "updatedAt": "2023-09-17T10:57:11.751Z"
+ },
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T10:57:24.446Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/Cycle.json b/integrations/linear/src/payload-examples/Cycle.json
new file mode 100644
index 00000000000..800ddd612f7
--- /dev/null
+++ b/integrations/linear/src/payload-examples/Cycle.json
@@ -0,0 +1,23 @@
+{
+ "data": {
+ "id": "cf1f0ba0-a769-402c-a5a4-308bca3ff53c",
+ "endsAt": "2023-10-08T23:00:00.000Z",
+ "number": 1,
+ "teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
+ "startsAt": "2023-09-24T23:00:00.000Z",
+ "createdAt": "2023-09-17T11:00:19.616Z",
+ "updatedAt": "2023-09-17T11:00:19.616Z",
+ "scopeHistory": [],
+ "issueCountHistory": [],
+ "completedScopeHistory": [],
+ "inProgressScopeHistory": [],
+ "completedIssueCountHistory": [],
+ "uncompletedIssuesUponCloseIds": []
+ },
+ "type": "Cycle",
+ "action": "create",
+ "createdAt": "2023-09-17T11:00:19.616Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:00:19.696Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/CycleCreated.json b/integrations/linear/src/payload-examples/CycleCreated.json
new file mode 100644
index 00000000000..800ddd612f7
--- /dev/null
+++ b/integrations/linear/src/payload-examples/CycleCreated.json
@@ -0,0 +1,23 @@
+{
+ "data": {
+ "id": "cf1f0ba0-a769-402c-a5a4-308bca3ff53c",
+ "endsAt": "2023-10-08T23:00:00.000Z",
+ "number": 1,
+ "teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
+ "startsAt": "2023-09-24T23:00:00.000Z",
+ "createdAt": "2023-09-17T11:00:19.616Z",
+ "updatedAt": "2023-09-17T11:00:19.616Z",
+ "scopeHistory": [],
+ "issueCountHistory": [],
+ "completedScopeHistory": [],
+ "inProgressScopeHistory": [],
+ "completedIssueCountHistory": [],
+ "uncompletedIssuesUponCloseIds": []
+ },
+ "type": "Cycle",
+ "action": "create",
+ "createdAt": "2023-09-17T11:00:19.616Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:00:19.696Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/CycleRemoved.json b/integrations/linear/src/payload-examples/CycleRemoved.json
new file mode 100644
index 00000000000..c918ca70497
--- /dev/null
+++ b/integrations/linear/src/payload-examples/CycleRemoved.json
@@ -0,0 +1,23 @@
+{
+ "data": {
+ "id": "fe7092dd-8a68-4a07-9477-b7c0a6f2136d",
+ "endsAt": "2023-11-13T00:00:00.000Z",
+ "number": 2,
+ "teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
+ "startsAt": "2023-10-15T23:00:00.000Z",
+ "createdAt": "2023-09-17T11:00:19.616Z",
+ "updatedAt": "2023-09-17T11:00:47.427Z",
+ "scopeHistory": [],
+ "issueCountHistory": [],
+ "completedScopeHistory": [],
+ "inProgressScopeHistory": [],
+ "completedIssueCountHistory": [],
+ "uncompletedIssuesUponCloseIds": []
+ },
+ "type": "Cycle",
+ "action": "remove",
+ "createdAt": "2023-09-17T11:01:08.772Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:01:08.807Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/CycleUpdated.json b/integrations/linear/src/payload-examples/CycleUpdated.json
new file mode 100644
index 00000000000..9ef3b49b379
--- /dev/null
+++ b/integrations/linear/src/payload-examples/CycleUpdated.json
@@ -0,0 +1,27 @@
+{
+ "data": {
+ "id": "fe7092dd-8a68-4a07-9477-b7c0a6f2136d",
+ "endsAt": "2023-11-13T00:00:00.000Z",
+ "number": 2,
+ "teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
+ "startsAt": "2023-10-15T23:00:00.000Z",
+ "createdAt": "2023-09-17T11:00:19.616Z",
+ "updatedAt": "2023-09-17T11:00:47.427Z",
+ "scopeHistory": [],
+ "issueCountHistory": [],
+ "completedScopeHistory": [],
+ "inProgressScopeHistory": [],
+ "completedIssueCountHistory": [],
+ "uncompletedIssuesUponCloseIds": []
+ },
+ "type": "Cycle",
+ "action": "update",
+ "createdAt": "2023-09-17T11:00:47.427Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "updatedFrom": {
+ "endsAt": "2023-11-12T23:00:00.000Z",
+ "updatedAt": "2023-09-17T11:00:47.414Z"
+ },
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:00:47.463Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/Issue.json b/integrations/linear/src/payload-examples/Issue.json
new file mode 100644
index 00000000000..bc64472006a
--- /dev/null
+++ b/integrations/linear/src/payload-examples/Issue.json
@@ -0,0 +1,42 @@
+{
+ "url": "https://linear.app/triggerdotdev/issue/TRI-1/welcome-to-ricks-world-👋",
+ "data": {
+ "id": "ee4d5612-0752-441a-95cd-2a0de56689d3",
+ "team": {
+ "id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
+ "key": "TRI",
+ "name": "Trigger.dev"
+ },
+ "state": {
+ "id": "8b77cd9c-f0cc-4be0-bd9b-52326d5c62a2",
+ "name": "Todo",
+ "type": "unstarted",
+ "color": "#e2e2e2"
+ },
+ "title": "Welcome to Rick's World ❤️🔥",
+ "labels": [],
+ "number": 1,
+ "teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
+ "stateId": "8b77cd9c-f0cc-4be0-bd9b-52326d5c62a2",
+ "labelIds": [],
+ "priority": 2,
+ "createdAt": "2023-09-17T10:43:20.862Z",
+ "sortOrder": -13701.52,
+ "updatedAt": "2023-09-17T10:52:49.321Z",
+ "boardOrder": 0,
+ "description": "Hi there. Complete these issues to learn how to use Linear and discover ✨**ProTips.** When you're done, delete them or move them to another team for others to view.\n\n### **To start, type** `C` to **create your first issue.**\n\nCreate issues from any view using `C` or by clicking the `New issue` button.\n\n \n\n[1189b618-97f2-4e2c-ae25-4f25467679e7](https://uploads.linear.app/fe63b3e2-bf87-46c0-8784-cd7d639287c8/532d146d-bcd6-4602-bf1f-83f674b70fff/1189b618-97f2-4e2c-ae25-4f25467679e7)\n\nOur issue editor and comments support Markdown. You can also: \n\n* @mention a teammate\n* Drag & drop images or video (Loom & Youtube embed automatically)\n* Use emoji ✅",
+ "priorityLabel": "High",
+ "subscriberIds": [],
+ "previousIdentifiers": []
+ },
+ "type": "Issue",
+ "action": "update",
+ "createdAt": "2023-09-17T10:52:49.321Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "updatedFrom": {
+ "title": "Welcome to Linear 👋",
+ "updatedAt": "2023-09-17T10:52:16.952Z"
+ },
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T10:52:49.762Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/IssueCreated.json b/integrations/linear/src/payload-examples/IssueCreated.json
new file mode 100644
index 00000000000..1884d12c207
--- /dev/null
+++ b/integrations/linear/src/payload-examples/IssueCreated.json
@@ -0,0 +1,41 @@
+{
+ "url": "https://linear.app/triggerdotdev/issue/TRI-10/you-know-the-rules",
+ "data": {
+ "id": "4ceeeecb-3442-4972-b27a-4ac7198d4eac",
+ "team": {
+ "id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
+ "key": "TRI",
+ "name": "Trigger.dev"
+ },
+ "state": {
+ "id": "6b6fd762-cc10-4da8-9775-dfc0784d2a3b",
+ "name": "Backlog",
+ "type": "backlog",
+ "color": "#bec2c8"
+ },
+ "title": "You know the rules",
+ "labels": [],
+ "number": 10,
+ "teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
+ "stateId": "6b6fd762-cc10-4da8-9775-dfc0784d2a3b",
+ "labelIds": [],
+ "priority": 0,
+ "createdAt": "2023-09-17T11:02:48.688Z",
+ "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "sortOrder": -81,
+ "updatedAt": "2023-09-17T11:02:48.688Z",
+ "boardOrder": 0,
+ "description": "And so do I",
+ "priorityLabel": "No priority",
+ "subscriberIds": [
+ "b552c442-62c9-40da-9932-ed8cb6e15a3f"
+ ],
+ "previousIdentifiers": []
+ },
+ "type": "Issue",
+ "action": "create",
+ "createdAt": "2023-09-17T11:02:48.688Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:02:48.800Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/IssueLabel.json b/integrations/linear/src/payload-examples/IssueLabel.json
new file mode 100644
index 00000000000..b083ebd7c1b
--- /dev/null
+++ b/integrations/linear/src/payload-examples/IssueLabel.json
@@ -0,0 +1,17 @@
+{
+ "data": {
+ "id": "ef8d9e0d-7286-460f-b398-7aaf7d182c16",
+ "name": "Give You Up",
+ "color": "#26b5ce",
+ "createdAt": "2023-09-17T11:08:22.426Z",
+ "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "updatedAt": "2023-09-17T11:08:22.426Z",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534"
+ },
+ "type": "IssueLabel",
+ "action": "create",
+ "createdAt": "2023-09-17T11:08:22.426Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:08:22.463Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/IssueLabelCreated.json b/integrations/linear/src/payload-examples/IssueLabelCreated.json
new file mode 100644
index 00000000000..b083ebd7c1b
--- /dev/null
+++ b/integrations/linear/src/payload-examples/IssueLabelCreated.json
@@ -0,0 +1,17 @@
+{
+ "data": {
+ "id": "ef8d9e0d-7286-460f-b398-7aaf7d182c16",
+ "name": "Give You Up",
+ "color": "#26b5ce",
+ "createdAt": "2023-09-17T11:08:22.426Z",
+ "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "updatedAt": "2023-09-17T11:08:22.426Z",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534"
+ },
+ "type": "IssueLabel",
+ "action": "create",
+ "createdAt": "2023-09-17T11:08:22.426Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:08:22.463Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/IssueLabelRemoved.json b/integrations/linear/src/payload-examples/IssueLabelRemoved.json
new file mode 100644
index 00000000000..b968f24d7c1
--- /dev/null
+++ b/integrations/linear/src/payload-examples/IssueLabelRemoved.json
@@ -0,0 +1,17 @@
+{
+ "data": {
+ "id": "ef8d9e0d-7286-460f-b398-7aaf7d182c16",
+ "name": "Let You Down",
+ "color": "#f2994a",
+ "createdAt": "2023-09-17T11:08:22.426Z",
+ "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "updatedAt": "2023-09-17T11:08:31.484Z",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534"
+ },
+ "type": "IssueLabel",
+ "action": "remove",
+ "createdAt": "2023-09-17T11:08:35.517Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:08:35.556Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/IssueLabelUpdated.json b/integrations/linear/src/payload-examples/IssueLabelUpdated.json
new file mode 100644
index 00000000000..234385f2fe7
--- /dev/null
+++ b/integrations/linear/src/payload-examples/IssueLabelUpdated.json
@@ -0,0 +1,22 @@
+{
+ "data": {
+ "id": "ef8d9e0d-7286-460f-b398-7aaf7d182c16",
+ "name": "Let You Down",
+ "color": "#f2994a",
+ "createdAt": "2023-09-17T11:08:22.426Z",
+ "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "updatedAt": "2023-09-17T11:08:31.484Z",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534"
+ },
+ "type": "IssueLabel",
+ "action": "update",
+ "createdAt": "2023-09-17T11:08:31.484Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "updatedFrom": {
+ "name": "Give You Up",
+ "color": "#26b5ce",
+ "updatedAt": "2023-09-17T11:08:22.426Z"
+ },
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:08:31.527Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/IssueRemoved.json b/integrations/linear/src/payload-examples/IssueRemoved.json
new file mode 100644
index 00000000000..1458e07836c
--- /dev/null
+++ b/integrations/linear/src/payload-examples/IssueRemoved.json
@@ -0,0 +1,42 @@
+{
+ "data": {
+ "id": "4ceeeecb-3442-4972-b27a-4ac7198d4eac",
+ "team": {
+ "id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
+ "key": "TRI",
+ "name": "Trigger.dev"
+ },
+ "state": {
+ "id": "6b6fd762-cc10-4da8-9775-dfc0784d2a3b",
+ "name": "Backlog",
+ "type": "backlog",
+ "color": "#bec2c8"
+ },
+ "title": "You know the rules",
+ "labels": [],
+ "number": 10,
+ "teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
+ "stateId": "6b6fd762-cc10-4da8-9775-dfc0784d2a3b",
+ "trashed": true,
+ "labelIds": [],
+ "priority": 0,
+ "createdAt": "2023-09-17T11:02:48.688Z",
+ "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "sortOrder": -81,
+ "updatedAt": "2023-09-17T11:02:48.689Z",
+ "archivedAt": "2023-09-17T11:02:58.487Z",
+ "boardOrder": 0,
+ "description": "And so do I",
+ "priorityLabel": "No priority",
+ "subscriberIds": [
+ "b552c442-62c9-40da-9932-ed8cb6e15a3f"
+ ],
+ "previousIdentifiers": []
+ },
+ "type": "Issue",
+ "action": "remove",
+ "createdAt": "2023-09-17T11:02:58.487Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:02:58.564Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/IssueUpdated.json b/integrations/linear/src/payload-examples/IssueUpdated.json
new file mode 100644
index 00000000000..bc64472006a
--- /dev/null
+++ b/integrations/linear/src/payload-examples/IssueUpdated.json
@@ -0,0 +1,42 @@
+{
+ "url": "https://linear.app/triggerdotdev/issue/TRI-1/welcome-to-ricks-world-👋",
+ "data": {
+ "id": "ee4d5612-0752-441a-95cd-2a0de56689d3",
+ "team": {
+ "id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
+ "key": "TRI",
+ "name": "Trigger.dev"
+ },
+ "state": {
+ "id": "8b77cd9c-f0cc-4be0-bd9b-52326d5c62a2",
+ "name": "Todo",
+ "type": "unstarted",
+ "color": "#e2e2e2"
+ },
+ "title": "Welcome to Rick's World ❤️🔥",
+ "labels": [],
+ "number": 1,
+ "teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
+ "stateId": "8b77cd9c-f0cc-4be0-bd9b-52326d5c62a2",
+ "labelIds": [],
+ "priority": 2,
+ "createdAt": "2023-09-17T10:43:20.862Z",
+ "sortOrder": -13701.52,
+ "updatedAt": "2023-09-17T10:52:49.321Z",
+ "boardOrder": 0,
+ "description": "Hi there. Complete these issues to learn how to use Linear and discover ✨**ProTips.** When you're done, delete them or move them to another team for others to view.\n\n### **To start, type** `C` to **create your first issue.**\n\nCreate issues from any view using `C` or by clicking the `New issue` button.\n\n \n\n[1189b618-97f2-4e2c-ae25-4f25467679e7](https://uploads.linear.app/fe63b3e2-bf87-46c0-8784-cd7d639287c8/532d146d-bcd6-4602-bf1f-83f674b70fff/1189b618-97f2-4e2c-ae25-4f25467679e7)\n\nOur issue editor and comments support Markdown. You can also: \n\n* @mention a teammate\n* Drag & drop images or video (Loom & Youtube embed automatically)\n* Use emoji ✅",
+ "priorityLabel": "High",
+ "subscriberIds": [],
+ "previousIdentifiers": []
+ },
+ "type": "Issue",
+ "action": "update",
+ "createdAt": "2023-09-17T10:52:49.321Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "updatedFrom": {
+ "title": "Welcome to Linear 👋",
+ "updatedAt": "2023-09-17T10:52:16.952Z"
+ },
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T10:52:49.762Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/Project.json b/integrations/linear/src/payload-examples/Project.json
new file mode 100644
index 00000000000..0d80b98a184
--- /dev/null
+++ b/integrations/linear/src/payload-examples/Project.json
@@ -0,0 +1,38 @@
+{
+ "url": "https://linear.app/triggerdotdev/project/ricks-world-1e55e5e64512",
+ "data": {
+ "id": "708440e6-648c-40f9-be87-9e00bbd8d1ec",
+ "name": "Rick's World",
+ "color": "#bec2c8",
+ "state": "backlog",
+ "leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "slugId": "1e55e5e64512",
+ "teamIds": [
+ "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f"
+ ],
+ "roadmaps": [],
+ "createdAt": "2023-09-17T11:24:43.171Z",
+ "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "memberIds": [
+ "b552c442-62c9-40da-9932-ed8cb6e15a3f"
+ ],
+ "sortOrder": 8161.55,
+ "updatedAt": "2023-09-17T11:24:43.171Z",
+ "milestones": [],
+ "description": "We know the game and we're gonna play it",
+ "scopeHistory": [],
+ "slackNewIssue": true,
+ "issueCountHistory": [],
+ "slackIssueComments": true,
+ "slackIssueStatuses": true,
+ "completedScopeHistory": [],
+ "inProgressScopeHistory": [],
+ "completedIssueCountHistory": []
+ },
+ "type": "Project",
+ "action": "create",
+ "createdAt": "2023-09-17T11:24:43.171Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:24:43.228Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/ProjectCreated.json b/integrations/linear/src/payload-examples/ProjectCreated.json
new file mode 100644
index 00000000000..0d80b98a184
--- /dev/null
+++ b/integrations/linear/src/payload-examples/ProjectCreated.json
@@ -0,0 +1,38 @@
+{
+ "url": "https://linear.app/triggerdotdev/project/ricks-world-1e55e5e64512",
+ "data": {
+ "id": "708440e6-648c-40f9-be87-9e00bbd8d1ec",
+ "name": "Rick's World",
+ "color": "#bec2c8",
+ "state": "backlog",
+ "leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "slugId": "1e55e5e64512",
+ "teamIds": [
+ "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f"
+ ],
+ "roadmaps": [],
+ "createdAt": "2023-09-17T11:24:43.171Z",
+ "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "memberIds": [
+ "b552c442-62c9-40da-9932-ed8cb6e15a3f"
+ ],
+ "sortOrder": 8161.55,
+ "updatedAt": "2023-09-17T11:24:43.171Z",
+ "milestones": [],
+ "description": "We know the game and we're gonna play it",
+ "scopeHistory": [],
+ "slackNewIssue": true,
+ "issueCountHistory": [],
+ "slackIssueComments": true,
+ "slackIssueStatuses": true,
+ "completedScopeHistory": [],
+ "inProgressScopeHistory": [],
+ "completedIssueCountHistory": []
+ },
+ "type": "Project",
+ "action": "create",
+ "createdAt": "2023-09-17T11:24:43.171Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:24:43.228Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/ProjectRemoved.json b/integrations/linear/src/payload-examples/ProjectRemoved.json
new file mode 100644
index 00000000000..f458fa0e672
--- /dev/null
+++ b/integrations/linear/src/payload-examples/ProjectRemoved.json
@@ -0,0 +1,40 @@
+{
+ "data": {
+ "id": "708440e6-648c-40f9-be87-9e00bbd8d1ec",
+ "name": "Rick's World",
+ "color": "#bec2c8",
+ "state": "backlog",
+ "leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "slugId": "1e55e5e64512",
+ "teamIds": [
+ "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f"
+ ],
+ "trashed": true,
+ "roadmaps": [],
+ "createdAt": "2023-09-17T11:24:43.171Z",
+ "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "memberIds": [
+ "b552c442-62c9-40da-9932-ed8cb6e15a3f"
+ ],
+ "sortOrder": 8161.55,
+ "updatedAt": "2023-09-17T11:25:09.384Z",
+ "archivedAt": "2023-09-17T11:25:16.188Z",
+ "milestones": [],
+ "targetDate": "2024-02-14T00:00:00.000Z",
+ "description": "We know the game and we're gonna play it",
+ "scopeHistory": [],
+ "slackNewIssue": true,
+ "issueCountHistory": [],
+ "slackIssueComments": true,
+ "slackIssueStatuses": true,
+ "completedScopeHistory": [],
+ "inProgressScopeHistory": [],
+ "completedIssueCountHistory": []
+ },
+ "type": "Project",
+ "action": "remove",
+ "createdAt": "2023-09-17T11:25:16.188Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:25:16.358Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/ProjectUpdate.json b/integrations/linear/src/payload-examples/ProjectUpdate.json
new file mode 100644
index 00000000000..c4ff2e297f3
--- /dev/null
+++ b/integrations/linear/src/payload-examples/ProjectUpdate.json
@@ -0,0 +1,47 @@
+{
+ "data": {
+ "id": "e5b2ae4e-acb5-4c6c-a22a-56c883e353b4",
+ "body": "We're no strangers to love",
+ "user": {
+ "id": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "name": "Rick Astley"
+ },
+ "health": "onTrack",
+ "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "project": {
+ "id": "54dac280-389f-4339-8d5d-de347a56eb79",
+ "name": "Rick's World"
+ },
+ "roadmaps": [],
+ "createdAt": "2023-09-17T11:28:23.406Z",
+ "projectId": "54dac280-389f-4339-8d5d-de347a56eb79",
+ "updatedAt": "2023-09-17T11:28:23.406Z",
+ "infoSnapshot": {
+ "state": "backlog",
+ "leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "memberIds": [
+ "b552c442-62c9-40da-9932-ed8cb6e15a3f"
+ ],
+ "teamsInfo": [
+ {
+ "id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
+ "issueInfo": {
+ "triageCount": 0,
+ "backlogCount": 0,
+ "startedCount": 0,
+ "canceledCount": 0,
+ "completedCount": 0,
+ "unstartedCount": 0
+ }
+ }
+ ],
+ "milestonesInfo": []
+ }
+ },
+ "type": "ProjectUpdate",
+ "action": "create",
+ "createdAt": "2023-09-17T11:28:23.406Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:28:23.479Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/ProjectUpdateCreated.json b/integrations/linear/src/payload-examples/ProjectUpdateCreated.json
new file mode 100644
index 00000000000..c4ff2e297f3
--- /dev/null
+++ b/integrations/linear/src/payload-examples/ProjectUpdateCreated.json
@@ -0,0 +1,47 @@
+{
+ "data": {
+ "id": "e5b2ae4e-acb5-4c6c-a22a-56c883e353b4",
+ "body": "We're no strangers to love",
+ "user": {
+ "id": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "name": "Rick Astley"
+ },
+ "health": "onTrack",
+ "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "project": {
+ "id": "54dac280-389f-4339-8d5d-de347a56eb79",
+ "name": "Rick's World"
+ },
+ "roadmaps": [],
+ "createdAt": "2023-09-17T11:28:23.406Z",
+ "projectId": "54dac280-389f-4339-8d5d-de347a56eb79",
+ "updatedAt": "2023-09-17T11:28:23.406Z",
+ "infoSnapshot": {
+ "state": "backlog",
+ "leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "memberIds": [
+ "b552c442-62c9-40da-9932-ed8cb6e15a3f"
+ ],
+ "teamsInfo": [
+ {
+ "id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
+ "issueInfo": {
+ "triageCount": 0,
+ "backlogCount": 0,
+ "startedCount": 0,
+ "canceledCount": 0,
+ "completedCount": 0,
+ "unstartedCount": 0
+ }
+ }
+ ],
+ "milestonesInfo": []
+ }
+ },
+ "type": "ProjectUpdate",
+ "action": "create",
+ "createdAt": "2023-09-17T11:28:23.406Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:28:23.479Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/ProjectUpdateRemoved.json b/integrations/linear/src/payload-examples/ProjectUpdateRemoved.json
new file mode 100644
index 00000000000..1e74f5f4015
--- /dev/null
+++ b/integrations/linear/src/payload-examples/ProjectUpdateRemoved.json
@@ -0,0 +1,48 @@
+{
+ "data": {
+ "id": "e5b2ae4e-acb5-4c6c-a22a-56c883e353b4",
+ "body": "You know the rules and so do I",
+ "user": {
+ "id": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "name": "Rick Astley"
+ },
+ "health": "onTrack",
+ "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "project": {
+ "id": "54dac280-389f-4339-8d5d-de347a56eb79",
+ "name": "Rick's World"
+ },
+ "editedAt": "2023-09-17T11:28:39.142Z",
+ "roadmaps": [],
+ "createdAt": "2023-09-17T11:28:23.406Z",
+ "projectId": "54dac280-389f-4339-8d5d-de347a56eb79",
+ "updatedAt": "2023-09-17T11:28:39.142Z",
+ "infoSnapshot": {
+ "state": "backlog",
+ "leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "memberIds": [
+ "b552c442-62c9-40da-9932-ed8cb6e15a3f"
+ ],
+ "teamsInfo": [
+ {
+ "id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
+ "issueInfo": {
+ "triageCount": 0,
+ "backlogCount": 0,
+ "startedCount": 0,
+ "canceledCount": 0,
+ "completedCount": 0,
+ "unstartedCount": 0
+ }
+ }
+ ],
+ "milestonesInfo": []
+ }
+ },
+ "type": "ProjectUpdate",
+ "action": "remove",
+ "createdAt": "2023-09-17T11:28:45.146Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:28:45.213Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/ProjectUpdateUpdated.json b/integrations/linear/src/payload-examples/ProjectUpdateUpdated.json
new file mode 100644
index 00000000000..d862c25d937
--- /dev/null
+++ b/integrations/linear/src/payload-examples/ProjectUpdateUpdated.json
@@ -0,0 +1,53 @@
+{
+ "data": {
+ "id": "e5b2ae4e-acb5-4c6c-a22a-56c883e353b4",
+ "body": "You know the rules and so do I",
+ "user": {
+ "id": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "name": "Rick Astley"
+ },
+ "health": "onTrack",
+ "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "project": {
+ "id": "54dac280-389f-4339-8d5d-de347a56eb79",
+ "name": "Rick's World"
+ },
+ "editedAt": "2023-09-17T11:28:39.142Z",
+ "roadmaps": [],
+ "createdAt": "2023-09-17T11:28:23.406Z",
+ "projectId": "54dac280-389f-4339-8d5d-de347a56eb79",
+ "updatedAt": "2023-09-17T11:28:39.142Z",
+ "infoSnapshot": {
+ "state": "backlog",
+ "leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "memberIds": [
+ "b552c442-62c9-40da-9932-ed8cb6e15a3f"
+ ],
+ "teamsInfo": [
+ {
+ "id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
+ "issueInfo": {
+ "triageCount": 0,
+ "backlogCount": 0,
+ "startedCount": 0,
+ "canceledCount": 0,
+ "completedCount": 0,
+ "unstartedCount": 0
+ }
+ }
+ ],
+ "milestonesInfo": []
+ }
+ },
+ "type": "ProjectUpdate",
+ "action": "update",
+ "createdAt": "2023-09-17T11:28:39.142Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "updatedFrom": {
+ "body": "We're no strangers to love",
+ "editedAt": null,
+ "updatedAt": "2023-09-17T11:28:23.406Z"
+ },
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:28:39.181Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/ProjectUpdated.json b/integrations/linear/src/payload-examples/ProjectUpdated.json
new file mode 100644
index 00000000000..47d995094d9
--- /dev/null
+++ b/integrations/linear/src/payload-examples/ProjectUpdated.json
@@ -0,0 +1,43 @@
+{
+ "url": "https://linear.app/triggerdotdev/project/ricks-world-1e55e5e64512",
+ "data": {
+ "id": "708440e6-648c-40f9-be87-9e00bbd8d1ec",
+ "name": "Rick's World",
+ "color": "#bec2c8",
+ "state": "backlog",
+ "leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "slugId": "1e55e5e64512",
+ "teamIds": [
+ "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f"
+ ],
+ "roadmaps": [],
+ "createdAt": "2023-09-17T11:24:43.171Z",
+ "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "memberIds": [
+ "b552c442-62c9-40da-9932-ed8cb6e15a3f"
+ ],
+ "sortOrder": 8161.55,
+ "updatedAt": "2023-09-17T11:25:09.383Z",
+ "milestones": [],
+ "targetDate": "2024-02-14T00:00:00.000Z",
+ "description": "We know the game and we're gonna play it",
+ "scopeHistory": [],
+ "slackNewIssue": true,
+ "issueCountHistory": [],
+ "slackIssueComments": true,
+ "slackIssueStatuses": true,
+ "completedScopeHistory": [],
+ "inProgressScopeHistory": [],
+ "completedIssueCountHistory": []
+ },
+ "type": "Project",
+ "action": "update",
+ "createdAt": "2023-09-17T11:25:09.383Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "updatedFrom": {
+ "updatedAt": "2023-09-17T11:24:43.171Z",
+ "targetDate": null
+ },
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:25:09.419Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/Reaction.json b/integrations/linear/src/payload-examples/Reaction.json
new file mode 100644
index 00000000000..5cc6aff2be9
--- /dev/null
+++ b/integrations/linear/src/payload-examples/Reaction.json
@@ -0,0 +1,24 @@
+{
+ "data": {
+ "id": "a22b3a1f-109f-427f-aa59-78b4ab1d2944",
+ "user": {
+ "id": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "name": "Rick Astley"
+ },
+ "emoji": "heart",
+ "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "comment": {
+ "id": "3f44c353-a3a1-45a4-b5da-259818ae21dd",
+ "body": "Never gonna",
+ "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f"
+ },
+ "createdAt": "2023-09-17T11:31:27.394Z",
+ "updatedAt": "2023-09-17T11:31:27.394Z"
+ },
+ "type": "Reaction",
+ "action": "create",
+ "createdAt": "2023-09-17T11:31:27.394Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:31:27.477Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/ReactionCreated.json b/integrations/linear/src/payload-examples/ReactionCreated.json
new file mode 100644
index 00000000000..856260578ac
--- /dev/null
+++ b/integrations/linear/src/payload-examples/ReactionCreated.json
@@ -0,0 +1,24 @@
+{
+ "data": {
+ "id": "a22b3a1f-109f-427f-aa59-78b4ab1d2944",
+ "user": {
+ "id": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "name": "Rick Astley"
+ },
+ "emoji": "heart",
+ "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "comment": {
+ "id": "3f44c353-a3a1-45a4-b5da-259818ae21dd",
+ "body": "Never gonna!",
+ "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f"
+ },
+ "createdAt": "2023-09-17T11:31:27.394Z",
+ "updatedAt": "2023-09-17T11:31:27.394Z"
+ },
+ "type": "Reaction",
+ "action": "create",
+ "createdAt": "2023-09-17T11:31:27.394Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:31:27.477Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/ReactionRemoved.json b/integrations/linear/src/payload-examples/ReactionRemoved.json
new file mode 100644
index 00000000000..60faace2506
--- /dev/null
+++ b/integrations/linear/src/payload-examples/ReactionRemoved.json
@@ -0,0 +1,24 @@
+{
+ "data": {
+ "id": "a22b3a1f-109f-427f-aa59-78b4ab1d2944",
+ "user": {
+ "id": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "name": "Rick Astley"
+ },
+ "emoji": "heart",
+ "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "comment": {
+ "id": "3f44c353-a3a1-45a4-b5da-259818ae21dd",
+ "body": "Never gonna!",
+ "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f"
+ },
+ "createdAt": "2023-09-17T11:31:27.394Z",
+ "updatedAt": "2023-09-17T11:31:27.394Z"
+ },
+ "type": "Reaction",
+ "action": "remove",
+ "createdAt": "2023-09-17T11:32:07.003Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T11:32:07.088Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/ReactionUpdated.json b/integrations/linear/src/payload-examples/ReactionUpdated.json
new file mode 100644
index 00000000000..50c86998640
--- /dev/null
+++ b/integrations/linear/src/payload-examples/ReactionUpdated.json
@@ -0,0 +1,28 @@
+{
+ "data": {
+ "id": "a22b3a1f-109f-427f-aa59-78b4ab1d2944",
+ "user": {
+ "id": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "name": "Rick Astley"
+ },
+ "emoji": "heart_on_fire",
+ "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
+ "comment": {
+ "id": "3f44c353-a3a1-45a4-b5da-259818ae21dd",
+ "body": "Never gonna!",
+ "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f"
+ },
+ "createdAt": "2023-09-17T12:31:27.394Z",
+ "updatedAt": "2023-09-17T12:32:14.394Z"
+ },
+ "type": "Reaction",
+ "action": "update",
+ "createdAt": "2023-09-17T12:31:27.394Z",
+ "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
+ "updatedFrom": {
+ "emoji": "heart",
+ "updatedAt": "2023-09-17T12:31:27.394Z"
+ },
+ "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
+ "webhookTimestamp": "2023-09-17T12:31:27.477Z"
+}
\ No newline at end of file
diff --git a/integrations/linear/src/payload-examples/index.ts b/integrations/linear/src/payload-examples/index.ts
new file mode 100644
index 00000000000..ffdab1b2b84
--- /dev/null
+++ b/integrations/linear/src/payload-examples/index.ts
@@ -0,0 +1,154 @@
+import { EventSpecificationExample } from "@trigger.dev/sdk";
+
+import AttachmentCreated from "./AttachmentCreated.json"
+import AttachmentRemoved from "./AttachmentRemoved.json"
+import AttachmentUpdated from "./AttachmentUpdated.json"
+import CommentCreated from "./CommentCreated.json"
+import CommentRemoved from "./CommentRemoved.json"
+import CommentUpdated from "./CommentUpdated.json"
+import CycleCreated from "./CycleCreated.json"
+import CycleRemoved from "./CycleRemoved.json"
+import CycleUpdated from "./CycleUpdated.json"
+import IssueCreated from "./IssueCreated.json"
+import IssueRemoved from "./IssueRemoved.json"
+import IssueUpdated from "./IssueUpdated.json"
+import IssueLabelCreated from "./IssueLabelCreated.json"
+import IssueLabelRemoved from "./IssueLabelRemoved.json"
+import IssueLabelUpdated from "./IssueLabelUpdated.json"
+import ProjectCreated from "./ProjectCreated.json"
+import ProjectRemoved from "./ProjectRemoved.json"
+import ProjectUpdated from "./ProjectUpdated.json"
+import ProjectUpdateCreated from "./ProjectUpdateCreated.json"
+import ProjectUpdateRemoved from "./ProjectUpdateRemoved.json"
+import ProjectUpdateUpdated from "./ProjectUpdateUpdated.json"
+import ReactionCreated from "./ReactionCreated.json"
+import ReactionRemoved from "./ReactionRemoved.json"
+import ReactionUpdated from "./ReactionUpdated.json"
+
+export const attachmentCreated: EventSpecificationExample = {
+ id: "AttachmentCreated",
+ name: "Attachment created",
+ payload: AttachmentCreated,
+};
+export const attachmentRemoved: EventSpecificationExample = {
+ id: "AttachmentRemoved",
+ name: "Attachment removed",
+ payload: AttachmentRemoved,
+};
+export const attachmentUpdated: EventSpecificationExample = {
+ id: "AttachmentUpdated",
+ name: "Attachment updated",
+ payload: AttachmentUpdated,
+};
+
+export const commentCreated: EventSpecificationExample = {
+ id: "CommentCreated",
+ name: "Comment created",
+ payload: CommentCreated,
+};
+export const commentRemoved: EventSpecificationExample = {
+ id: "CommentRemoved",
+ name: "Comment removed",
+ payload: CommentRemoved,
+};
+export const commentUpdated: EventSpecificationExample = {
+ id: "CommentUpdated",
+ name: "Comment updated",
+ payload: CommentUpdated,
+};
+
+export const cycleCreated: EventSpecificationExample = {
+ id: "CycleCreated",
+ name: "Cycle created",
+ payload: CycleCreated,
+};
+export const cycleRemoved: EventSpecificationExample = {
+ id: "CycleRemoved",
+ name: "Cycle removed",
+ payload: CycleRemoved,
+};
+export const cycleUpdated: EventSpecificationExample = {
+ id: "CycleUpdated",
+ name: "Cycle updated",
+ payload: CycleUpdated,
+};
+
+export const issueCreated: EventSpecificationExample = {
+ id: "IssueCreated",
+ name: "Issue created",
+ payload: IssueCreated,
+};
+export const issueRemoved: EventSpecificationExample = {
+ id: "IssueRemoved",
+ name: "Issue removed",
+ payload: IssueRemoved,
+};
+export const issueUpdated: EventSpecificationExample = {
+ id: "IssueUpdated",
+ name: "Issue updated",
+ payload: IssueUpdated,
+};
+
+export const issueLabelCreated: EventSpecificationExample = {
+ id: "IssueLabelCreated",
+ name: "IssueLabel created",
+ payload: IssueLabelCreated,
+};
+export const issueLabelRemoved: EventSpecificationExample = {
+ id: "IssueLabelRemoved",
+ name: "IssueLabel removed",
+ payload: IssueLabelRemoved,
+};
+export const issueLabelUpdated: EventSpecificationExample = {
+ id: "IssueLabelUpdated",
+ name: "IssueLabel updated",
+ payload: IssueLabelUpdated,
+};
+
+export const projectCreated: EventSpecificationExample = {
+ id: "ProjectCreated",
+ name: "Project created",
+ payload: ProjectCreated,
+};
+export const projectRemoved: EventSpecificationExample = {
+ id: "ProjectRemoved",
+ name: "Project removed",
+ payload: ProjectRemoved,
+};
+export const projectUpdated: EventSpecificationExample = {
+ id: "ProjectUpdated",
+ name: "Project updated",
+ payload: ProjectUpdated,
+};
+
+export const projectUpdateCreated: EventSpecificationExample = {
+ id: "ProjectUpdateCreated",
+ name: "ProjectUpdate created",
+ payload: ProjectUpdateCreated,
+};
+export const projectUpdateRemoved: EventSpecificationExample = {
+ id: "ProjectUpdateRemoved",
+ name: "ProjectUpdate removed",
+ payload: ProjectUpdateRemoved,
+};
+export const projectUpdateUpdated: EventSpecificationExample = {
+ id: "ProjectUpdateUpdated",
+ name: "ProjectUpdate updated",
+ payload: ProjectUpdateUpdated,
+};
+
+export const reactionCreated: EventSpecificationExample = {
+ id: "ReactionCreated",
+ name: "Reaction created",
+ payload: ReactionCreated,
+};
+export const reactionRemoved: EventSpecificationExample = {
+ id: "ReactionRemoved",
+ name: "Reaction removed",
+ payload: ReactionRemoved,
+};
+export const reactionUpdated: EventSpecificationExample = {
+ id: "ReactionUpdated",
+ name: "Reaction updated",
+ payload: ReactionUpdated,
+};
\ No newline at end of file
diff --git a/integrations/linear/src/schemas.ts b/integrations/linear/src/schemas.ts
new file mode 100644
index 00000000000..a33cfd796ad
--- /dev/null
+++ b/integrations/linear/src/schemas.ts
@@ -0,0 +1,444 @@
+import { z } from "zod";
+
+export const WebhookResourceTypeSchema = z.union([
+ z.literal("Attachment"),
+ z.literal("Comment"),
+ z.literal("Cycle"),
+ z.literal("Issue"),
+ z.literal("IssueLabel"),
+ z.literal("IssueSLA"),
+ z.literal("Project"),
+ z.literal("ProjectUpdate"),
+ z.literal("Reaction"),
+]);
+export type WebhookResourceType = z.infer;
+
+export const WebhookChangeActionTypeSchema = z.union([
+ z.literal("create"),
+ z.literal("remove"),
+ z.literal("update"),
+]);
+export type WebhookChangeActionType = z.infer;
+
+export const WebhookSLAActionTypeSchema = z.union([
+ z.literal("set"),
+ z.literal("breached"),
+ z.literal("highRisk"),
+]);
+export type WebhookSLAActionType = z.infer;
+
+export const WebhookActionTypeSchema = WebhookChangeActionTypeSchema.or(WebhookSLAActionTypeSchema);
+export type WebhookActionType = z.infer;
+
+const IssueLabelDataSchema = z.object({
+ archivedAt: z.coerce.date().optional().nullable(),
+ color: z.string(),
+ createdAt: z.coerce.date(),
+ creatorId: z.string().optional().nullable(),
+ description: z.string().optional().nullable(),
+ id: z.string(),
+ // isGroup: z.boolean(), // missing
+ name: z.string(),
+ organizationId: z.string(),
+ parentId: z.string().optional().nullable(),
+ teamId: z.string().optional().nullable(),
+ updatedAt: z.coerce.date(),
+});
+
+const IssueDataSchema = z.object({
+ archivedAt: z.coerce.date().optional().nullable(),
+ assignee: z.object({ id: z.string(), name: z.string() }).optional().nullable(),
+ assigneeId: z.string().optional().nullable(),
+ autoArchivedAt: z.coerce.date().optional().nullable(),
+ autoClosedAt: z.coerce.date().optional().nullable(),
+ boardOrder: z.number(),
+ canceledAt: z.coerce.date().optional().nullable(),
+ completedAt: z.coerce.date().optional().nullable(),
+ createdAt: z.coerce.date(),
+ creatorId: z.string().optional().nullable(),
+ cycleId: z.string().optional().nullable(),
+ description: z.string().optional().nullable(),
+ dueDate: z.coerce.date().optional().nullable(), // timeless
+ estimate: z.number().optional().nullable(),
+ favoriteId: z.string().optional().nullable(),
+ id: z.string(),
+ labelIds: z.array(z.string()),
+ labels: z.array(IssueLabelDataSchema.pick({ id: true, color: true, name: true })),
+ number: z.number(),
+ parentId: z.string().optional().nullable(),
+ previousIdentifiers: z.array(z.string()),
+ priority: z.number(),
+ priorityLabel: z.string(),
+ projectId: z.string().optional().nullable(),
+ sortOrder: z.number(),
+ state: z.object({ id: z.string(), color: z.string(), name: z.string(), type: z.string() }),
+ startedAt: z.coerce.date().optional().nullable(),
+ stateId: z.string(),
+ subIssueSortOrder: z.number().optional().nullable(),
+ subscriberIds: z.array(z.string()),
+ team: z.object({ id: z.string(), key: z.string(), name: z.string() }),
+ teamId: z.string(),
+ title: z.string(),
+ trashed: z.boolean().optional().nullable(),
+ triagedAt: z.coerce.date().optional().nullable(),
+ updatedAt: z.coerce.date(),
+});
+
+/** **WARNING:** Still in alpha - use with caution! */
+const AttachmentDataSchema = z.object({
+ archivedAt: z.coerce.date().optional().nullable(),
+ createdAt: z.coerce.date(),
+ creatorId: z.string().optional().nullable(),
+ groupBySource: z.boolean(),
+ id: z.string(),
+ issueId: z.string(),
+ metadata: z.object({}).passthrough(), // JSONObject
+ source: z
+ .object({
+ type: z.string().nullable(),
+ imageUrl: z.string().url().nullable(),
+ })
+ .passthrough()
+ .partial()
+ .nullable(), // JSONObject
+ sourceType: z.string().optional().nullable(),
+ subtitle: z.string().optional().nullable(),
+ title: z.string(),
+ updatedAt: z.coerce.date(),
+ url: z.string().url(),
+});
+
+const CommentDataSchema = z.object({
+ archivedAt: z.coerce.date().optional().nullable(),
+ body: z.string(),
+ botActorId: z.string().optional().nullable(),
+ createdAt: z.coerce.date(),
+ editedAt: z.string().optional().nullable(),
+ id: z.string(),
+ issue: IssueDataSchema.pick({ id: true, title: true }),
+ issueId: z.string(),
+ parentId: z.string().optional().nullable(),
+ reactionData: z.array(z.object({}).passthrough()), // JSONObject
+ updatedAt: z.coerce.date(),
+ userId: z.string().optional().nullable(),
+});
+
+const ReactionDataSchema = z.object({
+ archivedAt: z.coerce.date().optional().nullable(),
+ comment: CommentDataSchema.pick({
+ id: true,
+ body: true,
+ userId: true,
+ })
+ .optional()
+ .nullable(), // missing from official schema
+ createdAt: z.coerce.date(),
+ emoji: z.string(),
+ id: z.string(),
+ updatedAt: z.coerce.date(),
+ user: z.object({ id: z.string(), name: z.string() }).optional().nullable(),
+ userId: z.string().optional().nullable(),
+});
+
+const MilestoneDataSchema = z.object({
+ archivedAt: z.coerce.date().optional().nullable(),
+ createdAt: z.coerce.date(),
+ description: z.string().optional().nullable(),
+ id: z.string(),
+ name: z.string(),
+ projectId: z.string().optional().nullable(),
+ sortOrder: z.number(),
+ targetDate: z.coerce.date().optional().nullable(), // timeless
+ updatedAt: z.coerce.date(),
+});
+
+const RoadmapDataSchema = z.object({
+ archivedAt: z.coerce.date().optional().nullable(),
+ color: z.string().optional().nullable(),
+ createdAt: z.coerce.date(),
+ creatorId: z.string(),
+ description: z.string().optional().nullable(),
+ id: z.string(),
+ name: z.string(),
+ organizationId: z.string(),
+ ownerId: z.string(),
+ slugId: z.string(),
+ sortOrder: z.number(),
+ updatedAt: z.coerce.date(),
+});
+
+const ProjectDataSchema = z.object({
+ archivedAt: z.coerce.date().optional().nullable(),
+ autoArchivedAt: z.coerce.date().optional().nullable(),
+ canceledAt: z.coerce.date().optional().nullable(),
+ color: z.string(),
+ completedAt: z.coerce.date().optional().nullable(),
+ completedIssueCountHistory: z.array(z.number()),
+ completedScopeHistory: z.array(z.number()),
+ content: z.string().optional().nullable(),
+ convertedFromIssueId: z.string().optional().nullable(),
+ createdAt: z.coerce.date(),
+ creatorId: z.string(),
+ description: z.string(),
+ icon: z.string().optional().nullable(),
+ id: z.string(),
+ inProgressScopeHistory: z.array(z.number()),
+ integrationsSettingsId: z.string().optional().nullable(),
+ issueCountHistory: z.array(z.number()),
+ leadId: z.string(),
+ memberIds: z.array(z.string()),
+ milestones: z.array(MilestoneDataSchema.pick({ id: true, name: true })), // at projectMilestones key in official schema
+ name: z.string(),
+ progress: z.number().optional().nullable(), // missing, should be NonNullable
+ projectUpdateRemindersPausedUntilAt: z.coerce.date().optional().nullable(),
+ roadmaps: z
+ .array(RoadmapDataSchema.pick({ id: true, name: true }))
+ .optional()
+ .nullable(), // missing from official schema
+ scope: z.number().optional().nullable(), // missing, should be NonNullable
+ scopeHistory: z.array(z.number()),
+ slackIssueComments: z.boolean(),
+ slackIssueStatuses: z.boolean(),
+ slackNewIssue: z.boolean(),
+ slugId: z.string(),
+ sortOrder: z.number(),
+ startDate: z.coerce.date().optional().nullable(), // timeless
+ startedAt: z.coerce.date().optional().nullable(),
+ state: z.string(),
+ targetDate: z.coerce.date().optional().nullable(), // timeless
+ teamIds: z.array(z.string()),
+ trashed: z.boolean().optional().nullable(),
+ updatedAt: z.coerce.date(),
+});
+
+const ProjectUpdateDataSchema = z.object({
+ archivedAt: z.coerce.date().optional().nullable(),
+ body: z.string(),
+ createdAt: z.coerce.date(),
+ // diff: z.any().optional().nullable(), // missing, "stringified" JSON but typed as Record
+ editedAt: z.coerce.date().optional().nullable(),
+ health: z.string(),
+ id: z.string(),
+ infoSnapshot: z.object({}).passthrough().optional().nullable(), // JSONObject, marked as "internal"
+ project: ProjectDataSchema.pick({ id: true, name: true }),
+ projectId: z.string(),
+ roadmaps: z
+ .array(RoadmapDataSchema.pick({ id: true, name: true }))
+ .optional()
+ .nullable(), // missing from official schema
+ updatedAt: z.coerce.date(),
+ user: z.object({ id: z.string(), name: z.string() }),
+ userId: z.string(),
+});
+
+const CycleDataSchema = z.object({
+ archivedAt: z.coerce.date().optional().nullable(),
+ autoArchivedAt: z.coerce.date().optional().nullable(),
+ completedAt: z.coerce.date().optional().nullable(),
+ completedIssueCountHistory: z.array(z.number()),
+ completedScopeHistory: z.array(z.number()),
+ createdAt: z.coerce.date(),
+ description: z.string().optional().nullable(),
+ endsAt: z.coerce.date(),
+ id: z.string(),
+ inProgressScopeHistory: z.array(z.number()),
+ issueCountHistory: z.array(z.number()),
+ name: z.string().optional().nullable(),
+ number: z.number(),
+ progress: z.number().optional().nullable(), // missing, should be NonNullable
+ scopeHistory: z.array(z.number()),
+ startsAt: z.coerce.date(),
+ teamId: z.string(),
+ uncompletedIssuesUponCloseIds: z.array(z.string()),
+ updatedAt: z.coerce.date(),
+});
+
+export const WebhookPayloadBaseSchema = z.object({
+ createdAt: z.coerce.date(),
+ organizationId: z.string().optional().nullable(), // missing from official schema - workspace id?
+ url: z.string().url().optional().nullable(),
+ webhookId: z.string(),
+ webhookTimestamp: z.coerce.date(),
+});
+
+const CREATE = z.literal("create");
+const REMOVE = z.literal("remove");
+const UPDATE = z.literal("update");
+
+/** **WARNING:** Still in alpha - use with caution! */
+export const AttachmentEventBaseSchema = WebhookPayloadBaseSchema.extend({
+ type: z.literal("Attachment"),
+ data: AttachmentDataSchema,
+});
+export const AttachmentEventSchema = z.discriminatedUnion("action", [
+ AttachmentEventBaseSchema.extend({
+ action: CREATE,
+ }),
+ AttachmentEventBaseSchema.extend({
+ action: REMOVE,
+ }),
+ AttachmentEventBaseSchema.extend({
+ action: UPDATE,
+ updatedFrom: AttachmentDataSchema.partial(),
+ }),
+]);
+export type AttachmentEvent = z.infer;
+
+export const CommentEventBaseSchema = WebhookPayloadBaseSchema.extend({
+ type: z.literal("Comment"),
+ data: CommentDataSchema,
+});
+export const CommentEventSchema = z.discriminatedUnion("action", [
+ CommentEventBaseSchema.extend({
+ action: CREATE,
+ }),
+ CommentEventBaseSchema.extend({
+ action: REMOVE,
+ }),
+ CommentEventBaseSchema.extend({
+ action: UPDATE,
+ updatedFrom: CommentDataSchema.partial(),
+ }),
+]);
+export type CommentEvent = z.infer;
+
+export const CycleEventBaseSchema = WebhookPayloadBaseSchema.extend({
+ type: z.literal("Cycle"),
+ data: CycleDataSchema,
+});
+export const CycleEventSchema = z.discriminatedUnion("action", [
+ CycleEventBaseSchema.extend({
+ action: CREATE,
+ }),
+ CycleEventBaseSchema.extend({
+ action: REMOVE,
+ }),
+ CycleEventBaseSchema.extend({
+ action: UPDATE,
+ updatedFrom: CycleDataSchema.partial(),
+ }),
+]);
+export type CycleEvent = z.infer;
+
+export const IssueEventBaseSchema = WebhookPayloadBaseSchema.extend({
+ type: z.literal("Issue"),
+ data: IssueDataSchema,
+});
+export const IssueEventSchema = z.discriminatedUnion("action", [
+ IssueEventBaseSchema.extend({
+ action: CREATE,
+ }),
+ IssueEventBaseSchema.extend({
+ action: REMOVE,
+ }),
+ IssueEventBaseSchema.extend({
+ action: UPDATE,
+ updatedFrom: IssueDataSchema.partial(),
+ }),
+]);
+export type IssueEvent = z.infer;
+
+export const IssueLabelEventBaseSchema = WebhookPayloadBaseSchema.extend({
+ type: z.literal("IssueLabel"),
+ data: IssueLabelDataSchema,
+});
+export const IssueLabelEventSchema = z.discriminatedUnion("action", [
+ IssueLabelEventBaseSchema.extend({
+ action: CREATE,
+ }),
+ IssueLabelEventBaseSchema.extend({
+ action: REMOVE,
+ }),
+ IssueLabelEventBaseSchema.extend({
+ action: UPDATE,
+ updatedFrom: IssueLabelDataSchema.partial(),
+ }),
+]);
+export type IssueLabelEvent = z.infer;
+
+// TODO: confirm this with real-world payload(s)
+export const IssueSLAEventBaseSchema = WebhookPayloadBaseSchema.extend({
+ type: z.literal("IssueSLA"),
+ issueData: IssueDataSchema,
+});
+export const IssueSLAEventSchema = z.discriminatedUnion("action", [
+ IssueSLAEventBaseSchema.extend({
+ action: z.literal("set"),
+ }),
+ IssueSLAEventBaseSchema.extend({
+ action: z.literal("highRisk"),
+ }),
+ IssueSLAEventBaseSchema.extend({
+ action: z.literal("breached"),
+ }),
+]);
+export type IssueSLAEvent = z.infer;
+export type IssueSLAEventBreached = Extract;
+
+export const ProjectEventBaseSchema = WebhookPayloadBaseSchema.extend({
+ type: z.literal("Project"),
+ data: ProjectDataSchema,
+});
+export const ProjectEventSchema = z.discriminatedUnion("action", [
+ ProjectEventBaseSchema.extend({
+ action: CREATE,
+ }),
+ ProjectEventBaseSchema.extend({
+ action: REMOVE,
+ }),
+ ProjectEventBaseSchema.extend({
+ action: UPDATE,
+ updatedFrom: ProjectDataSchema.partial(),
+ }),
+]);
+export type ProjectEvent = z.infer;
+
+export const ProjectUpdateEventBaseSchema = WebhookPayloadBaseSchema.extend({
+ type: z.literal("ProjectUpdate"),
+ data: ProjectUpdateDataSchema,
+});
+export const ProjectUpdateEventSchema = z.discriminatedUnion("action", [
+ ProjectUpdateEventBaseSchema.extend({
+ action: CREATE,
+ }),
+ ProjectUpdateEventBaseSchema.extend({
+ action: REMOVE,
+ }),
+ ProjectUpdateEventBaseSchema.extend({
+ action: UPDATE,
+ updatedFrom: ProjectUpdateDataSchema.partial(),
+ }),
+]);
+export type ProjectUpdateEvent = z.infer;
+
+export const ReactionEventBaseSchema = WebhookPayloadBaseSchema.extend({
+ type: z.literal("Reaction"),
+ data: ReactionDataSchema,
+});
+export const ReactionEventSchema = z.discriminatedUnion("action", [
+ ReactionEventBaseSchema.extend({
+ action: CREATE,
+ }),
+ ReactionEventBaseSchema.extend({
+ action: REMOVE,
+ }),
+ ReactionEventBaseSchema.extend({
+ action: UPDATE,
+ updatedFrom: ReactionDataSchema.partial(),
+ }),
+]);
+export type ReactionEvent = z.infer;
+
+export const WebhookPayloadSchema = z.union([
+ AttachmentEventSchema,
+ CommentEventSchema,
+ CycleEventSchema,
+ IssueEventSchema,
+ IssueLabelEventSchema,
+ IssueSLAEventSchema,
+ ProjectEventSchema,
+ ProjectUpdateEventSchema,
+ ReactionEventSchema,
+]);
+
+export type WebhookPayload = z.infer;
diff --git a/integrations/linear/src/types.ts b/integrations/linear/src/types.ts
new file mode 100644
index 00000000000..5cb63330687
--- /dev/null
+++ b/integrations/linear/src/types.ts
@@ -0,0 +1,28 @@
+import { Request } from "@linear/sdk";
+import { WebhookActionType, WebhookPayload } from "./schemas";
+
+export type GetLinearPayload<
+ TPayload extends WebhookPayload,
+ TAction extends any = any,
+> = TAction extends WebhookActionType ? Extract : TPayload;
+
+type FunctionKeys = {
+ [K in keyof T]: T[K] extends Function ? K : never;
+}[keyof T];
+
+export type SerializedLinearOutput = T extends object
+ ? T extends Array
+ ? Array>
+ : { [K in keyof T as Exclude | `_${string}`>]: SerializedLinearOutput }
+ : T;
+
+export type LinearReturnType<
+ TPayload extends Omit,
+ K extends unknown = unknown,
+> = Promise<
+ Awaited>>
+>;
+
+export type AwaitNested = Omit & {
+ [key in K]: Awaited;
+};
diff --git a/integrations/linear/src/utils.ts b/integrations/linear/src/utils.ts
new file mode 100644
index 00000000000..17cdccb8947
--- /dev/null
+++ b/integrations/linear/src/utils.ts
@@ -0,0 +1,60 @@
+import { CommentEvent, IssueEvent, WebhookPayload } from "./schemas";
+import { GetLinearPayload } from "./types";
+import { LinearDocument as L } from "@linear/sdk";
+
+export type QueryVariables = {
+ after: string;
+ before: string;
+ first: number;
+ includeArchived: boolean;
+ last: number;
+ orderBy: L.PaginationOrderBy;
+};
+
+export type Nullable = Partial<{
+ [K in keyof T]: T[K] | null;
+}>;
+
+export const onCommentProperties = (payload: GetLinearPayload) => {
+ return [
+ { label: "Comment ID", text: payload.data.id },
+ { label: "Issue ID", text: payload.data.issueId },
+ { label: "Issue Title", text: payload.data.issue.title, url: payload.url ?? undefined },
+ ];
+};
+
+export const onIssueProperties = (payload: GetLinearPayload) => {
+ return [
+ { label: "Issue ID", text: payload.data.id },
+ {
+ label: "Issue",
+ text: `[${payload.data.team.key}-${payload.data.number}] ${payload.data.title}`,
+ url: payload.url ?? undefined,
+ },
+ ];
+};
+
+export const queryProperties = (query: Nullable) => {
+ return [
+ ...(query.after ? [{ label: "After", text: query.after }] : []),
+ ...(query.before ? [{ label: "Before", text: query.before }] : []),
+ ...(query.first ? [{ label: "First", text: String(query.first) }] : []),
+ ...(query.last ? [{ label: "Last", text: String(query.last) }] : []),
+ ...(query.orderBy ? [{ label: "Order by", text: query.orderBy }] : []),
+ ...(query.includeArchived
+ ? [{ label: "Include archived", text: String(query.includeArchived) }]
+ : []),
+ ];
+};
+
+export const updatedFromProperties = (payload: WebhookPayload) => {
+ if (payload.action !== "update") return [];
+ return [
+ {
+ label: "Updated Keys",
+ text: Object.keys(payload.updatedFrom)
+ .filter((key) => !["editedAt", "updatedAt"].includes(key))
+ .join(", "),
+ },
+ ];
+};
diff --git a/integrations/linear/src/webhooks.ts b/integrations/linear/src/webhooks.ts
new file mode 100644
index 00000000000..d31f909d5d6
--- /dev/null
+++ b/integrations/linear/src/webhooks.ts
@@ -0,0 +1,315 @@
+import {
+ EventFilter,
+ ExternalSource,
+ ExternalSourceTrigger,
+ HandlerEvent,
+ IntegrationTaskKey,
+ Logger,
+} from "@trigger.dev/sdk";
+import {
+ LinearDocument as L,
+ LinearWebhooks,
+ LINEAR_WEBHOOK_SIGNATURE_HEADER,
+ LINEAR_WEBHOOK_TS_FIELD,
+ WebhookPayload,
+ DeletePayload,
+ Webhook,
+} from "@linear/sdk";
+import { z } from "zod";
+import * as events from "./events";
+import { Linear, LinearRunTask, serializeLinearOutput } from "./index";
+import { WebhookPayloadSchema } from "./schemas";
+import { LinearReturnType } from "./types";
+import { queryProperties } from "./utils";
+
+export class Webhooks {
+ runTask: LinearRunTask;
+
+ constructor(runTask: LinearRunTask) {
+ this.runTask = runTask;
+ }
+
+ webhook(key: IntegrationTaskKey, params: { id: string }): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client, task, io) => {
+ return serializeLinearOutput(await client.webhook(params.id));
+ },
+ {
+ name: "Get Webhook",
+ params,
+ properties: [{ label: "Webhook ID", text: params.id }],
+ }
+ );
+ }
+
+ webhooks(key: IntegrationTaskKey, params?: L.WebhooksQueryVariables): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client, task, io) => {
+ let connections = await client.webhooks(params);
+ const hooks = connections.nodes;
+ while (connections.pageInfo.hasNextPage) {
+ connections = await connections.fetchNext();
+ hooks.push(...connections.nodes);
+ }
+ return serializeLinearOutput(hooks);
+ },
+ {
+ name: "List Webhooks",
+ params,
+ properties: queryProperties(params ?? {}),
+ }
+ );
+ }
+
+ createWebhook(
+ key: IntegrationTaskKey,
+ params: L.WebhookCreateInput
+ ): LinearReturnType & { webhook: Webhook | undefined }> {
+ return this.runTask(
+ key,
+ async (client, task, io) => {
+ const payload = await client.createWebhook({ ...params, allPublicTeams: !params.teamId });
+ return serializeLinearOutput({
+ ...payload,
+ webhook: await payload.webhook,
+ });
+ },
+ {
+ name: "Create Webhook",
+ params,
+ properties: [
+ { label: "Webhook URL", text: params.url },
+ { label: "Resource Types", text: params.resourceTypes.join(", ") },
+ ],
+ }
+ );
+ }
+
+ deleteWebhook(key: IntegrationTaskKey, params: { id: string }): LinearReturnType {
+ return this.runTask(
+ key,
+ async (client, task, io) => {
+ return serializeLinearOutput(await client.deleteWebhook(params.id));
+ },
+ {
+ name: "Delete Webhook",
+ params,
+ properties: [{ label: "Webhook ID", text: params.id }],
+ }
+ );
+ }
+
+ updateWebhook(
+ key: IntegrationTaskKey,
+ params: { id: string; input: L.WebhookUpdateInput }
+ ): LinearReturnType & { webhook: Webhook | undefined }> {
+ return this.runTask(
+ key,
+ async (client, task) => {
+ const payload = await client.updateWebhook(params.id, params.input);
+ return serializeLinearOutput({
+ ...payload,
+ webhook: await payload.webhook,
+ });
+ },
+ {
+ name: "Update Webhook",
+ params,
+ properties: [
+ { label: "Webhook ID", text: params.id },
+ ...(params.input.url ? [{ label: "Webhook URL", text: params.input.url }] : []),
+ ...(params.input.resourceTypes
+ ? [{ label: "Resource Types", text: params.input.resourceTypes.join(", ") }]
+ : []),
+ ],
+ }
+ );
+ }
+}
+
+type LinearEvents = (typeof events)[keyof typeof events];
+
+export type TriggerParams = {
+ teamId?: string;
+ filter?: EventFilter;
+};
+
+type CreateTriggersResult = ExternalSourceTrigger<
+ TEventSpecification,
+ ReturnType
+>;
+
+export function createTrigger(
+ source: ReturnType,
+ event: TEventSpecification,
+ params: TriggerParams
+): CreateTriggersResult {
+ return new ExternalSourceTrigger({
+ event,
+ params,
+ source,
+ options: {},
+ });
+}
+
+const WebhookRegistrationDataSchema = z.object({
+ success: z.literal(true),
+ webhook: z.object({
+ id: z.string(),
+ enabled: z.boolean(),
+ }),
+});
+
+export function createWebhookEventSource(
+ integration: Linear
+): ExternalSource {
+ return new ExternalSource("HTTP", {
+ id: "linear.webhook",
+ schema: z.object({
+ teamId: z.string().optional(),
+ }),
+ version: "0.1.0",
+ integration,
+ key: (params) => `${params.teamId ? params.teamId : "all"}`,
+ handler: webhookHandler,
+ register: async (event, io, ctx) => {
+ const { params, source: httpSource, options } = event;
+
+ // (key-specific) stored data, undefined if not registered yet
+ const webhookData = WebhookRegistrationDataSchema.safeParse(httpSource.data);
+
+ // set of events to register
+ const allEvents = Array.from(new Set([...options.event.desired, ...options.event.missing]));
+ const registeredOptions = {
+ event: allEvents,
+ };
+
+ // easily identify webhooks on linear
+ const label = `trigger.${params.teamId ? params.teamId : "all"}`;
+
+ if (httpSource.active && webhookData.success) {
+ const hasMissingOptions = Object.values(options).some(
+ (option) => option.missing.length > 0
+ );
+ if (!hasMissingOptions) return;
+
+ const updatedWebhook = await io.integration.updateWebhook("update-webhook", {
+ id: webhookData.data.webhook.id,
+ input: {
+ label,
+ resourceTypes: allEvents,
+ secret: httpSource.secret,
+ url: httpSource.url,
+ },
+ });
+
+ return {
+ data: WebhookRegistrationDataSchema.parse(updatedWebhook),
+ options: registeredOptions,
+ };
+ }
+
+ // check for existing hooks that match url
+ const listResponse = await io.integration.webhooks("list-webhooks");
+ const existingWebhook = listResponse.find((w) => w.url === httpSource.url);
+
+ if (existingWebhook) {
+ const updatedWebhook = await io.integration.updateWebhook("update-webhook", {
+ id: existingWebhook.id,
+ input: {
+ label,
+ resourceTypes: allEvents,
+ secret: httpSource.secret,
+ url: httpSource.url,
+ },
+ });
+
+ return {
+ data: WebhookRegistrationDataSchema.parse(updatedWebhook),
+ options: registeredOptions,
+ };
+ }
+
+ const createPayload = await io.integration.createWebhook("create-webhook", {
+ label,
+ resourceTypes: allEvents,
+ secret: httpSource.secret,
+ teamId: params.teamId,
+ url: httpSource.url,
+ });
+
+ return {
+ data: WebhookRegistrationDataSchema.parse(createPayload),
+ secret: (await createPayload.webhook)?.secret,
+ options: registeredOptions,
+ };
+ },
+ });
+}
+
+async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integration: Linear) {
+ logger.debug("[@trigger.dev/linear] Handling webhook payload");
+
+ const { rawEvent: request, source } = event;
+
+ const LINEAR_IPS = ["35.231.147.226", "35.243.134.228"];
+
+ const clientIp =
+ request.headers.get("cf-connecting-ip") ??
+ (
+ request.headers.get("x-real-ip") ??
+ request.headers.get("x-forwarded-for") ??
+ // default to allowing request if expected headers missing
+ LINEAR_IPS[0]
+ ).split(",")[0];
+
+ if (!LINEAR_IPS.includes(clientIp)) {
+ logger.error("[@trigger.dev/linear] Error validating webhook source, IP invalid.");
+ throw Error("[@trigger.dev/linear] Invalid source IP.");
+ }
+
+ const payloadUuid = request.headers.get("Linear-Delivery");
+ const payloadEvent = request.headers.get("Linear-Event");
+
+ if (!payloadUuid || !payloadEvent) {
+ logger.debug("[@trigger.dev/linear] Missing required Linear headers");
+ return { events: [] };
+ }
+
+ if (!request.body) {
+ logger.debug("[@trigger.dev/linear] No body found");
+ return { events: [] };
+ }
+
+ const signature = request.headers.get(LINEAR_WEBHOOK_SIGNATURE_HEADER);
+
+ if (!signature) {
+ logger.error("[@trigger.dev/linear] Error validating webhook signature, no signature found");
+ throw Error("[@trigger.dev/linear] No signature found");
+ }
+
+ const rawBody = await request.text();
+ const body = JSON.parse(rawBody);
+ const webhookHelper = new LinearWebhooks(source.secret);
+
+ if (!webhookHelper.verify(Buffer.from(rawBody), signature, body[LINEAR_WEBHOOK_TS_FIELD])) {
+ logger.error("[@trigger.dev/linear] Error validating webhook signature, they don't match");
+ throw Error("[@trigger.dev/linear] Invalid signature");
+ }
+
+ const webhookPayload = WebhookPayloadSchema.parse(body);
+
+ return {
+ events: [
+ {
+ id: payloadUuid,
+ name: payloadEvent,
+ source: "linear.app",
+ payload: webhookPayload,
+ context: {},
+ },
+ ],
+ };
+}
diff --git a/integrations/linear/tsconfig.json b/integrations/linear/tsconfig.json
new file mode 100644
index 00000000000..ba887725547
--- /dev/null
+++ b/integrations/linear/tsconfig.json
@@ -0,0 +1,33 @@
+{
+ "compilerOptions": {
+ "composite": false,
+ "declaration": false,
+ "declarationMap": false,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "inlineSources": false,
+ "isolatedModules": true,
+ "moduleResolution": "node16",
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "preserveWatchOutput": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true,
+ "sourceMap": true,
+ "resolveJsonModule": true,
+ "lib": [
+ "es2019"
+ ],
+ "module": "commonjs",
+ "target": "es2021"
+ },
+ "include": [
+ "./src/**/*.ts",
+ "tsup.config.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
\ No newline at end of file
diff --git a/integrations/linear/tsup.config.ts b/integrations/linear/tsup.config.ts
new file mode 100644
index 00000000000..0869b214866
--- /dev/null
+++ b/integrations/linear/tsup.config.ts
@@ -0,0 +1,24 @@
+
+import { defineConfig } from "tsup";
+
+export default defineConfig([
+ {
+ name: "main",
+ entry: ["./src/index.ts"],
+ outDir: "./dist",
+ platform: "node",
+ format: ["cjs"],
+ legacyOutput: true,
+ sourcemap: true,
+ clean: true,
+ bundle: true,
+ splitting: false,
+ dts: true,
+ treeshake: {
+ preset: "smallest",
+ },
+ esbuildPlugins: [],
+ external: ["http", "https", "util", "events", "tty", "os", "timers"],
+ },
+]);
+
diff --git a/packages/trigger-sdk/src/io.ts b/packages/trigger-sdk/src/io.ts
index 5e0101528f9..f9cd6de2380 100644
--- a/packages/trigger-sdk/src/io.ts
+++ b/packages/trigger-sdk/src/io.ts
@@ -57,7 +57,11 @@ export type RunTaskErrorCallback = (
error: unknown,
task: IOTask,
io: IO
-) => { retryAt: Date; error?: Error; jitter?: number } | Error | undefined | void;
+) =>
+ | { retryAt?: Date; error?: Error; jitter?: number; skipRetrying?: boolean }
+ | Error
+ | undefined
+ | void;
export class IO {
private _id: string;
@@ -627,6 +631,8 @@ export class IO {
throw error;
}
+ let skipRetrying = false;
+
if (onError) {
try {
const onErrorResult = onError(error, task, this);
@@ -635,13 +641,17 @@ export class IO {
if (onErrorResult instanceof Error) {
error = onErrorResult;
} else {
- const parsedError = ErrorWithStackSchema.safeParse(onErrorResult.error);
+ skipRetrying = !!onErrorResult.skipRetrying;
+
+ if (onErrorResult.retryAt && !skipRetrying) {
+ const parsedError = ErrorWithStackSchema.safeParse(onErrorResult.error);
- throw new RetryWithTaskError(
- parsedError.success ? parsedError.data : { message: "Unknown error" },
- task,
- onErrorResult.retryAt
- );
+ throw new RetryWithTaskError(
+ parsedError.success ? parsedError.data : { message: "Unknown error" },
+ task,
+ onErrorResult.retryAt
+ );
+ }
}
}
} catch (innerError) {
@@ -655,7 +665,7 @@ export class IO {
const parsedError = ErrorWithStackSchema.safeParse(error);
- if (options?.retry) {
+ if (options?.retry && !skipRetrying) {
const retryAt = calculateRetryAt(options.retry, task.attempts - 1);
if (retryAt) {
diff --git a/references/job-catalog/package.json b/references/job-catalog/package.json
index 7d77bf59527..1cdeac42245 100644
--- a/references/job-catalog/package.json
+++ b/references/job-catalog/package.json
@@ -21,6 +21,7 @@
"dynamic-schedule": "nodemon --watch src/dynamic-schedule.ts -r tsconfig-paths/register -r dotenv/config src/dynamic-schedule.ts",
"dynamic-triggers": "nodemon --watch src/dynamic-triggers.ts -r tsconfig-paths/register -r dotenv/config src/dynamic-triggers.ts",
"background-fetch": "nodemon --watch src/background-fetch.ts -r tsconfig-paths/register -r dotenv/config src/background-fetch.ts",
+ "linear": "nodemon --watch src/linear.ts -r tsconfig-paths/register -r dotenv/config src/linear.ts",
"status": "nodemon --watch src/status.ts -r tsconfig-paths/register -r dotenv/config src/status.ts",
"dev:trigger": "trigger-cli dev --port 8080"
},
@@ -39,7 +40,8 @@
"@types/node": "20.4.2",
"typescript": "5.1.6",
"zod": "3.21.4",
- "@trigger.dev/airtable": "workspace:*"
+ "@trigger.dev/airtable": "workspace:*",
+ "@trigger.dev/linear": "workspace:*"
},
"trigger.dev": {
"endpointId": "job-catalog"
diff --git a/references/job-catalog/src/linear.ts b/references/job-catalog/src/linear.ts
new file mode 100644
index 00000000000..25471b036b4
--- /dev/null
+++ b/references/job-catalog/src/linear.ts
@@ -0,0 +1,162 @@
+import { createExpressServer } from "@trigger.dev/express";
+import { TriggerClient, eventTrigger } from "@trigger.dev/sdk";
+import { Linear, PaginationOrderBy, serializeLinearOutput } from "@trigger.dev/linear";
+import { z } from "zod";
+
+export const client = new TriggerClient({
+ id: "job-catalog",
+ apiKey: process.env["TRIGGER_API_KEY"],
+ apiUrl: process.env["TRIGGER_API_URL"],
+ verbose: false,
+ ioLogLocalEnabled: true,
+});
+
+const linear = new Linear({
+ id: "linear",
+ apiKey: process.env["LINEAR_API_KEY"],
+});
+
+client.defineJob({
+ id: "linear-create-issue",
+ name: "Linear Create Issue",
+ version: "0.1.0",
+ integrations: { linear },
+ trigger: eventTrigger({
+ name: "linear.create.issue",
+ schema: z.object({
+ teamId: z.string().optional(),
+ issueTitle: z.string().optional(),
+ }),
+ }),
+ run: async (payload, io, ctx) => {
+ const firstTeam = await io.linear.runTask("get-first-team", async (client) => {
+ const payload = await client.teams();
+ //use helper to serialize raw client output
+ return serializeLinearOutput(payload.nodes[0]);
+ });
+
+ const issue = await io.linear.createIssue("create-issue", {
+ //use optional teamId if passed - ID of first team otherwise
+ teamId: payload.teamId ?? firstTeam.id,
+ title: payload.issueTitle ?? "Shiny new issue",
+ });
+
+ if (issue) {
+ //some time to visually inspect and trigger the next job
+ await io.wait("10 secs", 10);
+ await io.linear.deleteIssue("delete-issue", { id: issue.id });
+ } else {
+ io.logger.error("Failed to create issue, nothing to delete.");
+ }
+ },
+});
+
+client.defineJob({
+ id: "linear-new-issue-reply",
+ name: "Linear New Issue Reply",
+ version: "0.1.0",
+ integrations: {
+ linear,
+ },
+ //this should trigger when creating an issue in the job above
+ trigger: linear.onIssueCreated(),
+ run: async (payload, io, ctx) => {
+ const newIssueId = payload.data.id;
+
+ await io.linear.createComment("create-comment", {
+ issueId: newIssueId,
+ body: "Thank's for opening this issue!",
+ });
+
+ await io.linear.createReaction("create-reaction", {
+ issueId: newIssueId,
+ emoji: "+1",
+ });
+ },
+});
+
+client.defineJob({
+ id: "linear-test-misc",
+ name: "Linear Test Misc",
+ version: "0.1.0",
+ integrations: { linear },
+ //this should automatically trigger after the first example
+ trigger: linear.onIssueRemoved(),
+ run: async (payload, io, ctx) => {
+ //info about the currently authenticated user
+ await io.linear.viewer("get-viewer");
+ await io.linear.organization("get-org");
+
+ //create a team
+ const newTeam = await io.linear.createTeam("create-team", {
+ name: `Rickastleydotdev-${Math.floor(Math.random() * 1000)}`,
+ });
+
+ //get some entities
+ const comments = await io.linear.comments("get-comments", { first: 2 });
+ const issues = await io.linear.issues("get-issues", { first: 10 });
+ const projects = await io.linear.projects("get-projects", { first: 5 });
+
+ return {
+ deletedIssueId: payload.data.id,
+ newTeamKey: newTeam?.key,
+ comments: comments.nodes.length,
+ issues: issues.nodes.length,
+ projects: projects.nodes.length,
+ };
+ },
+});
+
+client.defineJob({
+ id: "linear-pagination",
+ name: "Linear Pagination",
+ version: "0.1.0",
+ integrations: {
+ linear,
+ },
+ trigger: eventTrigger({
+ name: "linear.paginate",
+ }),
+ run: async (payload, io, ctx) => {
+ //the same params will be used for all tasks
+ const params = { first: 5, orderBy: PaginationOrderBy.UpdatedAt };
+
+ //1. Linear SDK
+ const sdkIssues = await io.linear.runTask("all-issues-via-sdk", async (client) => {
+ const edges = await client.issues(params);
+
+ //this will keep appending nodes until there are no more
+ while (edges.pageInfo.hasNextPage) {
+ await edges.fetchNext();
+ }
+
+ //use serialization helper to remove functions etc
+ return serializeLinearOutput(edges.nodes);
+ });
+
+ //2. Linear integration - no pagination helper
+ let edges = await io.linear.issues("get-issues", params);
+ let noHelper = edges.nodes;
+
+ for (let i = 0; edges.pageInfo.hasNextPage; i++) {
+ edges = await io.linear.issues(`get-more-issues-${i}`, {
+ ...params,
+ after: edges.pageInfo.endCursor,
+ });
+ noHelper = noHelper.concat(edges.nodes);
+ }
+
+ //3. Linear integration - with the pagination helper
+ const withHelper = await io.linear.getAll(io.linear.issues, "get-all", params);
+
+ return {
+ issueCounts: {
+ withSdk: sdkIssues.length,
+ noHelper: noHelper.length,
+ withHelper: withHelper.length,
+ },
+ };
+ },
+});
+
+createExpressServer(client);
diff --git a/references/job-catalog/tsconfig.json b/references/job-catalog/tsconfig.json
index 563b275458e..e38922dd977 100644
--- a/references/job-catalog/tsconfig.json
+++ b/references/job-catalog/tsconfig.json
@@ -96,6 +96,12 @@
],
"@trigger.dev/airtable/*": [
"../../integrations/airtable/src/*"
+ ],
+ "@trigger.dev/linear": [
+ "../../integrations/linear/src/index"
+ ],
+ "@trigger.dev/linear/*": [
+ "../../integrations/linear/src/*"
]
}
}