feat(triggers): add Linear v2 triggers with automatic webhook registration (#3991)

* feat(triggers): add Linear v2 triggers with automatic webhook registration

* fix(triggers): preserve specific Linear API error messages in catch block

* fix(triggers): check response.ok before JSON parsing, replace as any with as unknown

* fix linear subscription params

* fix build

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
This commit is contained in:
Waleed
2026-04-06 13:50:05 -07:00
committed by GitHub
parent 18a7868bb3
commit 5ea63f1607
21 changed files with 926 additions and 11 deletions

View File

@@ -8,8 +8,9 @@ import { getTrigger } from '@/triggers'
export const LinearBlock: BlockConfig<LinearResponse> = {
type: 'linear',
name: 'Linear',
name: 'Linear (Legacy)',
description: 'Interact with Linear issues, projects, and more',
hideFromToolbar: true,
authMode: AuthMode.OAuth,
triggerAllowed: true,
longDescription:
@@ -2543,3 +2544,62 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
],
},
}
/**
* Linear V2 Block
*
* Uses automatic webhook registration via the Linear GraphQL API.
* Inherits all tool operations from the legacy block.
*/
export const LinearV2Block: BlockConfig<LinearResponse> = {
...LinearBlock,
type: 'linear_v2',
name: 'Linear',
hideFromToolbar: false,
subBlocks: [
...LinearBlock.subBlocks.filter(
(sb) =>
!sb.id?.startsWith('webhookUrlDisplay') &&
!sb.id?.startsWith('webhookSecret') &&
!sb.id?.startsWith('triggerSave') &&
!sb.id?.startsWith('triggerInstructions') &&
!sb.id?.startsWith('selectedTriggerId')
),
// V2 Trigger SubBlocks
...getTrigger('linear_issue_created_v2').subBlocks,
...getTrigger('linear_issue_updated_v2').subBlocks,
...getTrigger('linear_issue_removed_v2').subBlocks,
...getTrigger('linear_comment_created_v2').subBlocks,
...getTrigger('linear_comment_updated_v2').subBlocks,
...getTrigger('linear_project_created_v2').subBlocks,
...getTrigger('linear_project_updated_v2').subBlocks,
...getTrigger('linear_cycle_created_v2').subBlocks,
...getTrigger('linear_cycle_updated_v2').subBlocks,
...getTrigger('linear_label_created_v2').subBlocks,
...getTrigger('linear_label_updated_v2').subBlocks,
...getTrigger('linear_project_update_created_v2').subBlocks,
...getTrigger('linear_customer_request_created_v2').subBlocks,
...getTrigger('linear_customer_request_updated_v2').subBlocks,
...getTrigger('linear_webhook_v2').subBlocks,
],
triggers: {
enabled: true,
available: [
'linear_issue_created_v2',
'linear_issue_updated_v2',
'linear_issue_removed_v2',
'linear_comment_created_v2',
'linear_comment_updated_v2',
'linear_project_created_v2',
'linear_project_updated_v2',
'linear_cycle_created_v2',
'linear_cycle_updated_v2',
'linear_label_created_v2',
'linear_label_updated_v2',
'linear_project_update_created_v2',
'linear_customer_request_created_v2',
'linear_customer_request_updated_v2',
'linear_webhook_v2',
],
},
}

View File

@@ -102,7 +102,7 @@ import { KnowledgeBlock } from '@/blocks/blocks/knowledge'
import { LangsmithBlock } from '@/blocks/blocks/langsmith'
import { LaunchDarklyBlock } from '@/blocks/blocks/launchdarkly'
import { LemlistBlock } from '@/blocks/blocks/lemlist'
import { LinearBlock } from '@/blocks/blocks/linear'
import { LinearBlock, LinearV2Block } from '@/blocks/blocks/linear'
import { LinkedInBlock } from '@/blocks/blocks/linkedin'
import { LinkupBlock } from '@/blocks/blocks/linkup'
import { LoopsBlock } from '@/blocks/blocks/loops'
@@ -338,6 +338,7 @@ export const registry: Record<string, BlockConfig> = {
launchdarkly: LaunchDarklyBlock,
lemlist: LemlistBlock,
linear: LinearBlock,
linear_v2: LinearV2Block,
linkedin: LinkedInBlock,
linkup: LinkupBlock,
loops: LoopsBlock,

View File

@@ -1,9 +1,15 @@
import crypto from 'crypto'
import { createLogger } from '@sim/logger'
import { safeCompare } from '@/lib/core/security/encryption'
import { generateId } from '@/lib/core/utils/uuid'
import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/providers/subscription-utils'
import type {
DeleteSubscriptionContext,
EventMatchContext,
FormatInputContext,
FormatInputResult,
SubscriptionContext,
SubscriptionResult,
WebhookProviderHandler,
} from '@/lib/webhooks/providers/types'
import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
@@ -60,6 +66,169 @@ export const linearHandler: WebhookProviderHandler = {
}
},
async matchEvent({ body, requestId, providerConfig }: EventMatchContext) {
const triggerId = providerConfig.triggerId as string | undefined
if (triggerId && !triggerId.endsWith('_webhook') && !triggerId.endsWith('_webhook_v2')) {
const { isLinearEventMatch } = await import('@/triggers/linear/utils')
const obj = body as Record<string, unknown>
const action = obj.action as string | undefined
const type = obj.type as string | undefined
if (!isLinearEventMatch(triggerId, type || '', action)) {
logger.debug(
`[${requestId}] Linear event mismatch for trigger ${triggerId}. Type: ${type}, Action: ${action}. Skipping.`
)
return false
}
}
return true
},
async createSubscription(ctx: SubscriptionContext): Promise<SubscriptionResult | undefined> {
const config = getProviderConfig(ctx.webhook)
const triggerId = config.triggerId as string | undefined
if (!triggerId || !triggerId.endsWith('_v2')) {
return undefined
}
const apiKey = config.apiKey as string | undefined
if (!apiKey) {
logger.warn(`[${ctx.requestId}] Missing API key for Linear webhook ${ctx.webhook.id}`)
throw new Error(
'Linear API key is required. Please provide a valid API key in the trigger configuration.'
)
}
const { LINEAR_RESOURCE_TYPE_MAP } = await import('@/triggers/linear/utils')
const resourceTypes = LINEAR_RESOURCE_TYPE_MAP[triggerId]
if (!resourceTypes) {
logger.warn(`[${ctx.requestId}] Unknown Linear trigger ID: ${triggerId}`)
throw new Error(`Unknown Linear trigger type: ${triggerId}`)
}
const notificationUrl = getNotificationUrl(ctx.webhook)
const webhookSecret = generateId()
const teamId = config.teamId as string | undefined
const input: Record<string, unknown> = {
url: notificationUrl,
resourceTypes,
secret: webhookSecret,
enabled: true,
}
if (teamId) {
input.teamId = teamId
} else {
input.allPublicTeams = true
}
try {
const response = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: apiKey,
},
body: JSON.stringify({
query: `mutation WebhookCreate($input: WebhookCreateInput!) {
webhookCreate(input: $input) {
success
webhook { id enabled }
}
}`,
variables: { input },
}),
})
if (!response.ok) {
throw new Error(
`Linear API returned HTTP ${response.status}. Please verify your API key and try again.`
)
}
const data = await response.json()
const result = data?.data?.webhookCreate
if (!result?.success) {
const errors = data?.errors?.map((e: { message: string }) => e.message).join(', ')
logger.error(`[${ctx.requestId}] Failed to create Linear webhook`, {
errors,
webhookId: ctx.webhook.id,
})
throw new Error(errors || 'Failed to create Linear webhook. Please verify your API key.')
}
const externalId = result.webhook?.id
logger.info(
`[${ctx.requestId}] Created Linear webhook ${externalId} for webhook ${ctx.webhook.id}`
)
return {
providerConfigUpdates: {
externalId,
webhookSecret,
},
}
} catch (error) {
if (error instanceof Error && error.message !== 'fetch failed') {
throw error
}
logger.error(`[${ctx.requestId}] Error creating Linear webhook`, {
error: error instanceof Error ? error.message : String(error),
})
throw new Error('Failed to create Linear webhook. Please verify your API key and try again.')
}
},
async deleteSubscription(ctx: DeleteSubscriptionContext): Promise<void> {
const config = getProviderConfig(ctx.webhook)
const externalId = config.externalId as string | undefined
const apiKey = config.apiKey as string | undefined
if (!externalId || !apiKey) {
return
}
try {
const response = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: apiKey,
},
body: JSON.stringify({
query: `mutation WebhookDelete($id: String!) {
webhookDelete(id: $id) { success }
}`,
variables: { id: externalId },
}),
})
if (!response.ok) {
logger.warn(
`[${ctx.requestId}] Linear API returned HTTP ${response.status} during webhook deletion for ${externalId}`
)
return
}
const data = await response.json()
if (data?.data?.webhookDelete?.success) {
logger.info(
`[${ctx.requestId}] Deleted Linear webhook ${externalId} for webhook ${ctx.webhook.id}`
)
} else {
logger.warn(
`[${ctx.requestId}] Linear webhook deletion returned unsuccessful for ${externalId}`
)
}
} catch (error) {
logger.warn(`[${ctx.requestId}] Error deleting Linear webhook ${externalId} (non-fatal)`, {
error: error instanceof Error ? error.message : String(error),
})
}
},
extractIdempotencyId(body: unknown) {
const obj = body as Record<string, unknown>
const data = obj.data as Record<string, unknown> | undefined

View File

@@ -0,0 +1,30 @@
import { LinearIcon } from '@/components/icons'
import { buildCommentOutputs, buildLinearV2SubBlocks } from '@/triggers/linear/utils'
import type { TriggerConfig } from '@/triggers/types'
export const linearCommentCreatedV2Trigger: TriggerConfig = {
id: 'linear_comment_created_v2',
name: 'Linear Comment Created',
provider: 'linear',
description: 'Trigger workflow when a new comment is created in Linear',
version: '2.0.0',
icon: LinearIcon,
subBlocks: buildLinearV2SubBlocks({
triggerId: 'linear_comment_created_v2',
eventType: 'Comment (create)',
}),
outputs: buildCommentOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Linear-Event': 'Comment',
'Linear-Delivery': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'Linear-Signature': 'sha256...',
'User-Agent': 'Linear-Webhook',
},
},
}

View File

@@ -0,0 +1,30 @@
import { LinearIcon } from '@/components/icons'
import { buildCommentOutputs, buildLinearV2SubBlocks } from '@/triggers/linear/utils'
import type { TriggerConfig } from '@/triggers/types'
export const linearCommentUpdatedV2Trigger: TriggerConfig = {
id: 'linear_comment_updated_v2',
name: 'Linear Comment Updated',
provider: 'linear',
description: 'Trigger workflow when a comment is updated in Linear',
version: '2.0.0',
icon: LinearIcon,
subBlocks: buildLinearV2SubBlocks({
triggerId: 'linear_comment_updated_v2',
eventType: 'Comment (update)',
}),
outputs: buildCommentOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Linear-Event': 'Comment',
'Linear-Delivery': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'Linear-Signature': 'sha256...',
'User-Agent': 'Linear-Webhook',
},
},
}

View File

@@ -0,0 +1,30 @@
import { LinearIcon } from '@/components/icons'
import { buildCustomerRequestOutputs, buildLinearV2SubBlocks } from '@/triggers/linear/utils'
import type { TriggerConfig } from '@/triggers/types'
export const linearCustomerRequestCreatedV2Trigger: TriggerConfig = {
id: 'linear_customer_request_created_v2',
name: 'Linear Customer Request Created',
provider: 'linear',
description: 'Trigger workflow when a new customer request is created in Linear',
version: '2.0.0',
icon: LinearIcon,
subBlocks: buildLinearV2SubBlocks({
triggerId: 'linear_customer_request_created_v2',
eventType: 'Customer Requests',
}),
outputs: buildCustomerRequestOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Linear-Event': 'CustomerNeed',
'Linear-Delivery': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'Linear-Signature': 'sha256...',
'User-Agent': 'Linear-Webhook',
},
},
}

View File

@@ -0,0 +1,30 @@
import { LinearIcon } from '@/components/icons'
import { buildCustomerRequestOutputs, buildLinearV2SubBlocks } from '@/triggers/linear/utils'
import type { TriggerConfig } from '@/triggers/types'
export const linearCustomerRequestUpdatedV2Trigger: TriggerConfig = {
id: 'linear_customer_request_updated_v2',
name: 'Linear Customer Request Updated',
provider: 'linear',
description: 'Trigger workflow when a customer request is updated in Linear',
version: '2.0.0',
icon: LinearIcon,
subBlocks: buildLinearV2SubBlocks({
triggerId: 'linear_customer_request_updated_v2',
eventType: 'CustomerNeed (update)',
}),
outputs: buildCustomerRequestOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Linear-Event': 'CustomerNeed',
'Linear-Delivery': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'Linear-Signature': 'sha256...',
'User-Agent': 'Linear-Webhook',
},
},
}

View File

@@ -0,0 +1,30 @@
import { LinearIcon } from '@/components/icons'
import { buildCycleOutputs, buildLinearV2SubBlocks } from '@/triggers/linear/utils'
import type { TriggerConfig } from '@/triggers/types'
export const linearCycleCreatedV2Trigger: TriggerConfig = {
id: 'linear_cycle_created_v2',
name: 'Linear Cycle Created',
provider: 'linear',
description: 'Trigger workflow when a new cycle is created in Linear',
version: '2.0.0',
icon: LinearIcon,
subBlocks: buildLinearV2SubBlocks({
triggerId: 'linear_cycle_created_v2',
eventType: 'Cycle (create)',
}),
outputs: buildCycleOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Linear-Event': 'Cycle',
'Linear-Delivery': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'Linear-Signature': 'sha256...',
'User-Agent': 'Linear-Webhook',
},
},
}

View File

@@ -0,0 +1,30 @@
import { LinearIcon } from '@/components/icons'
import { buildCycleOutputs, buildLinearV2SubBlocks } from '@/triggers/linear/utils'
import type { TriggerConfig } from '@/triggers/types'
export const linearCycleUpdatedV2Trigger: TriggerConfig = {
id: 'linear_cycle_updated_v2',
name: 'Linear Cycle Updated',
provider: 'linear',
description: 'Trigger workflow when a cycle is updated in Linear',
version: '2.0.0',
icon: LinearIcon,
subBlocks: buildLinearV2SubBlocks({
triggerId: 'linear_cycle_updated_v2',
eventType: 'Cycle (update)',
}),
outputs: buildCycleOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Linear-Event': 'Cycle',
'Linear-Delivery': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'Linear-Signature': 'sha256...',
'User-Agent': 'Linear-Webhook',
},
},
}

View File

@@ -1,15 +1,30 @@
export { linearCommentCreatedTrigger } from './comment_created'
export { linearCommentCreatedV2Trigger } from './comment_created_v2'
export { linearCommentUpdatedTrigger } from './comment_updated'
export { linearCommentUpdatedV2Trigger } from './comment_updated_v2'
export { linearCustomerRequestCreatedTrigger } from './customer_request_created'
export { linearCustomerRequestCreatedV2Trigger } from './customer_request_created_v2'
export { linearCustomerRequestUpdatedTrigger } from './customer_request_updated'
export { linearCustomerRequestUpdatedV2Trigger } from './customer_request_updated_v2'
export { linearCycleCreatedTrigger } from './cycle_created'
export { linearCycleCreatedV2Trigger } from './cycle_created_v2'
export { linearCycleUpdatedTrigger } from './cycle_updated'
export { linearCycleUpdatedV2Trigger } from './cycle_updated_v2'
export { linearIssueCreatedTrigger } from './issue_created'
export { linearIssueCreatedV2Trigger } from './issue_created_v2'
export { linearIssueRemovedTrigger } from './issue_removed'
export { linearIssueRemovedV2Trigger } from './issue_removed_v2'
export { linearIssueUpdatedTrigger } from './issue_updated'
export { linearIssueUpdatedV2Trigger } from './issue_updated_v2'
export { linearLabelCreatedTrigger } from './label_created'
export { linearLabelCreatedV2Trigger } from './label_created_v2'
export { linearLabelUpdatedTrigger } from './label_updated'
export { linearLabelUpdatedV2Trigger } from './label_updated_v2'
export { linearProjectCreatedTrigger } from './project_created'
export { linearProjectCreatedV2Trigger } from './project_created_v2'
export { linearProjectUpdateCreatedTrigger } from './project_update_created'
export { linearProjectUpdateCreatedV2Trigger } from './project_update_created_v2'
export { linearProjectUpdatedTrigger } from './project_updated'
export { linearProjectUpdatedV2Trigger } from './project_updated_v2'
export { linearWebhookTrigger } from './webhook'
export { linearWebhookV2Trigger } from './webhook_v2'

View File

@@ -0,0 +1,37 @@
import { LinearIcon } from '@/components/icons'
import { buildIssueOutputs, buildLinearV2SubBlocks } from '@/triggers/linear/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Linear Issue Created Trigger (v2)
*
* Primary trigger - includes the dropdown for selecting trigger type.
* Uses automatic webhook registration via the Linear GraphQL API.
*/
export const linearIssueCreatedV2Trigger: TriggerConfig = {
id: 'linear_issue_created_v2',
name: 'Linear Issue Created',
provider: 'linear',
description: 'Trigger workflow when a new issue is created in Linear',
version: '2.0.0',
icon: LinearIcon,
subBlocks: buildLinearV2SubBlocks({
triggerId: 'linear_issue_created_v2',
eventType: 'Issue (create)',
includeDropdown: true,
}),
outputs: buildIssueOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Linear-Event': 'Issue',
'Linear-Delivery': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'Linear-Signature': 'sha256...',
'User-Agent': 'Linear-Webhook',
},
},
}

View File

@@ -0,0 +1,30 @@
import { LinearIcon } from '@/components/icons'
import { buildIssueOutputs, buildLinearV2SubBlocks } from '@/triggers/linear/utils'
import type { TriggerConfig } from '@/triggers/types'
export const linearIssueRemovedV2Trigger: TriggerConfig = {
id: 'linear_issue_removed_v2',
name: 'Linear Issue Removed',
provider: 'linear',
description: 'Trigger workflow when an issue is removed/deleted in Linear',
version: '2.0.0',
icon: LinearIcon,
subBlocks: buildLinearV2SubBlocks({
triggerId: 'linear_issue_removed_v2',
eventType: 'Issue (remove)',
}),
outputs: buildIssueOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Linear-Event': 'Issue',
'Linear-Delivery': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'Linear-Signature': 'sha256...',
'User-Agent': 'Linear-Webhook',
},
},
}

View File

@@ -0,0 +1,30 @@
import { LinearIcon } from '@/components/icons'
import { buildIssueOutputs, buildLinearV2SubBlocks } from '@/triggers/linear/utils'
import type { TriggerConfig } from '@/triggers/types'
export const linearIssueUpdatedV2Trigger: TriggerConfig = {
id: 'linear_issue_updated_v2',
name: 'Linear Issue Updated',
provider: 'linear',
description: 'Trigger workflow when an issue is updated in Linear',
version: '2.0.0',
icon: LinearIcon,
subBlocks: buildLinearV2SubBlocks({
triggerId: 'linear_issue_updated_v2',
eventType: 'Issue (update)',
}),
outputs: buildIssueOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Linear-Event': 'Issue',
'Linear-Delivery': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'Linear-Signature': 'sha256...',
'User-Agent': 'Linear-Webhook',
},
},
}

View File

@@ -0,0 +1,30 @@
import { LinearIcon } from '@/components/icons'
import { buildLabelOutputs, buildLinearV2SubBlocks } from '@/triggers/linear/utils'
import type { TriggerConfig } from '@/triggers/types'
export const linearLabelCreatedV2Trigger: TriggerConfig = {
id: 'linear_label_created_v2',
name: 'Linear Label Created',
provider: 'linear',
description: 'Trigger workflow when a new label is created in Linear',
version: '2.0.0',
icon: LinearIcon,
subBlocks: buildLinearV2SubBlocks({
triggerId: 'linear_label_created_v2',
eventType: 'IssueLabel (create)',
}),
outputs: buildLabelOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Linear-Event': 'IssueLabel',
'Linear-Delivery': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'Linear-Signature': 'sha256...',
'User-Agent': 'Linear-Webhook',
},
},
}

View File

@@ -0,0 +1,30 @@
import { LinearIcon } from '@/components/icons'
import { buildLabelOutputs, buildLinearV2SubBlocks } from '@/triggers/linear/utils'
import type { TriggerConfig } from '@/triggers/types'
export const linearLabelUpdatedV2Trigger: TriggerConfig = {
id: 'linear_label_updated_v2',
name: 'Linear Label Updated',
provider: 'linear',
description: 'Trigger workflow when a label is updated in Linear',
version: '2.0.0',
icon: LinearIcon,
subBlocks: buildLinearV2SubBlocks({
triggerId: 'linear_label_updated_v2',
eventType: 'IssueLabel (update)',
}),
outputs: buildLabelOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Linear-Event': 'IssueLabel',
'Linear-Delivery': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'Linear-Signature': 'sha256...',
'User-Agent': 'Linear-Webhook',
},
},
}

View File

@@ -0,0 +1,30 @@
import { LinearIcon } from '@/components/icons'
import { buildLinearV2SubBlocks, buildProjectOutputs } from '@/triggers/linear/utils'
import type { TriggerConfig } from '@/triggers/types'
export const linearProjectCreatedV2Trigger: TriggerConfig = {
id: 'linear_project_created_v2',
name: 'Linear Project Created',
provider: 'linear',
description: 'Trigger workflow when a new project is created in Linear',
version: '2.0.0',
icon: LinearIcon,
subBlocks: buildLinearV2SubBlocks({
triggerId: 'linear_project_created_v2',
eventType: 'Project (create)',
}),
outputs: buildProjectOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Linear-Event': 'Project',
'Linear-Delivery': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'Linear-Signature': 'sha256...',
'User-Agent': 'Linear-Webhook',
},
},
}

View File

@@ -0,0 +1,30 @@
import { LinearIcon } from '@/components/icons'
import { buildLinearV2SubBlocks, buildProjectUpdateOutputs } from '@/triggers/linear/utils'
import type { TriggerConfig } from '@/triggers/types'
export const linearProjectUpdateCreatedV2Trigger: TriggerConfig = {
id: 'linear_project_update_created_v2',
name: 'Linear Project Update Created',
provider: 'linear',
description: 'Trigger workflow when a new project update is posted in Linear',
version: '2.0.0',
icon: LinearIcon,
subBlocks: buildLinearV2SubBlocks({
triggerId: 'linear_project_update_created_v2',
eventType: 'ProjectUpdate (create)',
}),
outputs: buildProjectUpdateOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Linear-Event': 'ProjectUpdate',
'Linear-Delivery': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'Linear-Signature': 'sha256...',
'User-Agent': 'Linear-Webhook',
},
},
}

View File

@@ -0,0 +1,30 @@
import { LinearIcon } from '@/components/icons'
import { buildLinearV2SubBlocks, buildProjectOutputs } from '@/triggers/linear/utils'
import type { TriggerConfig } from '@/triggers/types'
export const linearProjectUpdatedV2Trigger: TriggerConfig = {
id: 'linear_project_updated_v2',
name: 'Linear Project Updated',
provider: 'linear',
description: 'Trigger workflow when a project is updated in Linear',
version: '2.0.0',
icon: LinearIcon,
subBlocks: buildLinearV2SubBlocks({
triggerId: 'linear_project_updated_v2',
eventType: 'Project (update)',
}),
outputs: buildProjectOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Linear-Event': 'Project',
'Linear-Delivery': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'Linear-Signature': 'sha256...',
'User-Agent': 'Linear-Webhook',
},
},
}

View File

@@ -1,3 +1,4 @@
import type { SubBlockConfig } from '@/blocks/types'
import type { TriggerOutput } from '@/triggers/types'
/**
@@ -22,7 +23,37 @@ export const linearTriggerOptions = [
]
/**
* Generate setup instructions for a specific Linear event type
* Maps trigger IDs to Linear resource types for webhook creation.
* Used by the automatic webhook registration in provider-subscriptions.
*/
export const LINEAR_RESOURCE_TYPE_MAP: Record<string, string[]> = {
linear_issue_created_v2: ['Issue'],
linear_issue_updated_v2: ['Issue'],
linear_issue_removed_v2: ['Issue'],
linear_comment_created_v2: ['Comment'],
linear_comment_updated_v2: ['Comment'],
linear_project_created_v2: ['Project'],
linear_project_updated_v2: ['Project'],
linear_cycle_created_v2: ['Cycle'],
linear_cycle_updated_v2: ['Cycle'],
linear_label_created_v2: ['IssueLabel'],
linear_label_updated_v2: ['IssueLabel'],
linear_project_update_created_v2: ['ProjectUpdate'],
linear_customer_request_created_v2: ['CustomerNeed'],
linear_customer_request_updated_v2: ['CustomerNeed'],
linear_webhook_v2: [
'Issue',
'Comment',
'Project',
'Cycle',
'IssueLabel',
'ProjectUpdate',
'CustomerNeed',
],
}
/**
* Generate setup instructions for manual Linear webhook configuration (v1 triggers)
*/
export function linearSetupInstructions(eventType: string, additionalNotes?: string): string {
const instructions = [
@@ -47,6 +78,121 @@ export function linearSetupInstructions(eventType: string, additionalNotes?: str
.join('')
}
/**
* Generate setup instructions for automatic Linear webhook creation (v2 triggers)
*/
export function linearV2SetupInstructions(eventType: string, additionalNotes?: string): string {
const instructions = [
'Enter your Linear API Key above. You can create one in Linear at <a href="https://linear.app/settings/api" target="_blank" rel="noopener noreferrer">Settings &gt; API &gt; Personal API keys</a>.',
'Optionally enter a <strong>Team ID</strong> to scope the webhook to a single team. Leave it empty to receive events from all public teams. You can find Team IDs in Linear under <a href="https://linear.app/settings" target="_blank" rel="noopener noreferrer">Settings &gt; Teams</a> or via the API.',
`Click <strong>"Save Configuration"</strong> to automatically create the webhook in Linear for <strong>${eventType}</strong> events.`,
'The webhook will be automatically deleted when you remove this trigger.',
]
if (additionalNotes) {
instructions.push(additionalNotes)
}
return instructions
.map(
(instruction, index) =>
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
)
.join('')
}
/**
* V2 trigger dropdown options with _v2 suffixed IDs
*/
export const linearV2TriggerOptions = [
{ label: 'Issue Created', id: 'linear_issue_created_v2' },
{ label: 'Issue Updated', id: 'linear_issue_updated_v2' },
{ label: 'Issue Removed', id: 'linear_issue_removed_v2' },
{ label: 'Comment Created', id: 'linear_comment_created_v2' },
{ label: 'Comment Updated', id: 'linear_comment_updated_v2' },
{ label: 'Project Created', id: 'linear_project_created_v2' },
{ label: 'Project Updated', id: 'linear_project_updated_v2' },
{ label: 'Cycle Created', id: 'linear_cycle_created_v2' },
{ label: 'Cycle Updated', id: 'linear_cycle_updated_v2' },
{ label: 'Label Created', id: 'linear_label_created_v2' },
{ label: 'Label Updated', id: 'linear_label_updated_v2' },
{ label: 'Project Update Created', id: 'linear_project_update_created_v2' },
{ label: 'Customer Request Created', id: 'linear_customer_request_created_v2' },
{ label: 'Customer Request Updated', id: 'linear_customer_request_updated_v2' },
{ label: 'General Webhook (All Events)', id: 'linear_webhook_v2' },
]
/**
* Builds the complete subBlocks array for a v2 Linear trigger.
* Webhooks are managed via API, so no webhook URL is displayed.
*
* Structure: [dropdown?] -> apiKey -> triggerSave -> instructions
*/
export function buildLinearV2SubBlocks(options: {
triggerId: string
eventType: string
includeDropdown?: boolean
additionalNotes?: string
}): SubBlockConfig[] {
const { triggerId, eventType, includeDropdown = false, additionalNotes } = options
const blocks: SubBlockConfig[] = []
if (includeDropdown) {
blocks.push({
id: 'selectedTriggerId',
title: 'Trigger Type',
type: 'dropdown',
mode: 'trigger',
options: linearV2TriggerOptions,
value: () => triggerId,
required: true,
})
}
blocks.push({
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your Linear API key',
password: true,
required: true,
paramVisibility: 'user-only',
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
})
blocks.push({
id: 'teamId',
title: 'Team ID',
type: 'short-input',
placeholder: 'All teams (optional)',
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
})
blocks.push({
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId,
condition: { field: 'selectedTriggerId', value: triggerId },
})
blocks.push({
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: linearV2SetupInstructions(eventType, additionalNotes),
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
})
return blocks
}
/**
* Shared user/actor output schema
* Note: Linear webhooks only include id, name, and type in actor objects
@@ -298,7 +444,7 @@ export function buildIssueOutputs(): Record<string, TriggerOutput> {
type: 'object',
description: 'Previous values for changed fields (only present on update)',
},
} as any
} as unknown as Record<string, TriggerOutput>
}
/**
@@ -385,7 +531,7 @@ export function buildCommentOutputs(): Record<string, TriggerOutput> {
type: 'object',
description: 'Previous values for changed fields (only present on update)',
},
} as any
} as unknown as Record<string, TriggerOutput>
}
/**
@@ -528,7 +674,7 @@ export function buildProjectOutputs(): Record<string, TriggerOutput> {
type: 'object',
description: 'Previous values for changed fields (only present on update)',
},
} as any
} as unknown as Record<string, TriggerOutput>
}
/**
@@ -631,7 +777,7 @@ export function buildCycleOutputs(): Record<string, TriggerOutput> {
type: 'object',
description: 'Previous values for changed fields (only present on update)',
},
} as any
} as unknown as Record<string, TriggerOutput>
}
/**
@@ -718,7 +864,7 @@ export function buildLabelOutputs(): Record<string, TriggerOutput> {
type: 'object',
description: 'Previous values for changed fields (only present on update)',
},
} as any
} as unknown as Record<string, TriggerOutput>
}
/**
@@ -793,7 +939,7 @@ export function buildProjectUpdateOutputs(): Record<string, TriggerOutput> {
type: 'object',
description: 'Previous values for changed fields (only present on update)',
},
} as any
} as unknown as Record<string, TriggerOutput>
}
/**
@@ -876,7 +1022,7 @@ export function buildCustomerRequestOutputs(): Record<string, TriggerOutput> {
type: 'object',
description: 'Previous values for changed fields (only present on update)',
},
} as any
} as unknown as Record<string, TriggerOutput>
}
/**
@@ -900,7 +1046,8 @@ export function isLinearEventMatch(triggerId: string, eventType: string, action?
linear_customer_request_updated: { type: 'CustomerNeed', actions: ['update'] },
}
const config = eventMap[triggerId]
const normalizedId = triggerId.replace(/_v2$/, '')
const config = eventMap[normalizedId]
if (!config) {
return true // Unknown trigger, allow through
}

View File

@@ -0,0 +1,66 @@
import { LinearIcon } from '@/components/icons'
import { buildLinearV2SubBlocks, userOutputs } from '@/triggers/linear/utils'
import type { TriggerConfig } from '@/triggers/types'
export const linearWebhookV2Trigger: TriggerConfig = {
id: 'linear_webhook_v2',
name: 'Linear Webhook',
provider: 'linear',
description: 'Trigger workflow from any Linear webhook event',
version: '2.0.0',
icon: LinearIcon,
subBlocks: buildLinearV2SubBlocks({
triggerId: 'linear_webhook_v2',
eventType: 'All Events',
additionalNotes:
'This webhook will receive all Linear events. Use the <code>type</code> and <code>action</code> fields in the payload to filter and handle different event types.',
}),
outputs: {
action: {
type: 'string',
description: 'Action performed (create, update, remove)',
},
type: {
type: 'string',
description: 'Entity type (Issue, Comment, Project, Cycle, IssueLabel, ProjectUpdate, etc.)',
},
webhookId: {
type: 'string',
description: 'Webhook ID',
},
webhookTimestamp: {
type: 'number',
description: 'Webhook timestamp (milliseconds)',
},
organizationId: {
type: 'string',
description: 'Organization ID',
},
createdAt: {
type: 'string',
description: 'Event creation timestamp',
},
actor: userOutputs,
data: {
type: 'object',
description: 'Complete entity data object',
},
updatedFrom: {
type: 'object',
description: 'Previous values for changed fields (only present on update)',
},
},
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Linear-Event': 'Issue',
'Linear-Delivery': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'Linear-Signature': 'sha256...',
'User-Agent': 'Linear-Webhook',
},
},
}

View File

@@ -170,20 +170,35 @@ import {
} from '@/triggers/lemlist'
import {
linearCommentCreatedTrigger,
linearCommentCreatedV2Trigger,
linearCommentUpdatedTrigger,
linearCommentUpdatedV2Trigger,
linearCustomerRequestCreatedTrigger,
linearCustomerRequestCreatedV2Trigger,
linearCustomerRequestUpdatedTrigger,
linearCustomerRequestUpdatedV2Trigger,
linearCycleCreatedTrigger,
linearCycleCreatedV2Trigger,
linearCycleUpdatedTrigger,
linearCycleUpdatedV2Trigger,
linearIssueCreatedTrigger,
linearIssueCreatedV2Trigger,
linearIssueRemovedTrigger,
linearIssueRemovedV2Trigger,
linearIssueUpdatedTrigger,
linearIssueUpdatedV2Trigger,
linearLabelCreatedTrigger,
linearLabelCreatedV2Trigger,
linearLabelUpdatedTrigger,
linearLabelUpdatedV2Trigger,
linearProjectCreatedTrigger,
linearProjectCreatedV2Trigger,
linearProjectUpdateCreatedTrigger,
linearProjectUpdateCreatedV2Trigger,
linearProjectUpdatedTrigger,
linearProjectUpdatedV2Trigger,
linearWebhookTrigger,
linearWebhookV2Trigger,
} from '@/triggers/linear'
import {
microsoftTeamsChatSubscriptionTrigger,
@@ -380,6 +395,21 @@ export const TRIGGER_REGISTRY: TriggerRegistry = {
linear_project_update_created: linearProjectUpdateCreatedTrigger,
linear_customer_request_created: linearCustomerRequestCreatedTrigger,
linear_customer_request_updated: linearCustomerRequestUpdatedTrigger,
linear_webhook_v2: linearWebhookV2Trigger,
linear_issue_created_v2: linearIssueCreatedV2Trigger,
linear_issue_updated_v2: linearIssueUpdatedV2Trigger,
linear_issue_removed_v2: linearIssueRemovedV2Trigger,
linear_comment_created_v2: linearCommentCreatedV2Trigger,
linear_comment_updated_v2: linearCommentUpdatedV2Trigger,
linear_project_created_v2: linearProjectCreatedV2Trigger,
linear_project_updated_v2: linearProjectUpdatedV2Trigger,
linear_cycle_created_v2: linearCycleCreatedV2Trigger,
linear_cycle_updated_v2: linearCycleUpdatedV2Trigger,
linear_label_created_v2: linearLabelCreatedV2Trigger,
linear_label_updated_v2: linearLabelUpdatedV2Trigger,
linear_project_update_created_v2: linearProjectUpdateCreatedV2Trigger,
linear_customer_request_created_v2: linearCustomerRequestCreatedV2Trigger,
linear_customer_request_updated_v2: linearCustomerRequestUpdatedV2Trigger,
microsoftteams_webhook: microsoftTeamsWebhookTrigger,
microsoftteams_chat_subscription: microsoftTeamsChatSubscriptionTrigger,
notion_page_created: notionPageCreatedTrigger,