From 7b9423afed1980030b1b96fb69c8d172d7a2c9a2 Mon Sep 17 00:00:00 2001 From: Vincent Grobler Date: Fri, 15 May 2026 13:55:09 +0100 Subject: [PATCH 1/2] feat: wire Linear webhook auto-register + docs - Auto-register Linear webhook on channel creation via linear-webhook-register edge function - Store webhook_secret and webhook_id in channel config - Show registration status banner in create form UI - Add Linear section to channels.md docs (setup, triggers, bidirectional flow, priority mapping) --- docs/channels.md | 113 +++++++++++++++++- .../settings/MessagingChannelsSettings.tsx | 67 ++++++++++- 2 files changed, 176 insertions(+), 4 deletions(-) diff --git a/docs/channels.md b/docs/channels.md index 323a34cb..00dbe692 100644 --- a/docs/channels.md +++ b/docs/channels.md @@ -11,6 +11,7 @@ Supported platforms: | [Discord](./discord-integration.md) | `/ask` slash command | Follow-up message (deferred) | | [Email](#email) | Email to a dedicated inbound address | Reply email via Resend | | [Trello](#trello) | Card created or moved to trigger list | Comment on the original card | +| [Linear](#linear) | Issue created, state change, or label applied | Comment on the original issue | --- @@ -40,7 +41,7 @@ Channels are separate from [Output Routes](./output-routes.md). Output routes pu ## Managed Bot vs Bring Your Own Bot (BYOB) -All channel platforms (except Email and Trello) support two modes: +All channel platforms (except Email, Trello, and Linear) support two modes: ### Managed Bot CrewForm hosts and operates the bot. You connect your chat to it using a **connect code** generated in Settings. Fast to set up — no bot registration needed. @@ -361,6 +362,116 @@ No additional environment variables are needed — all Trello credentials (API K --- +## Linear + +Linear channels trigger CrewForm agents when issues are created, moved to a specific state, or labelled. Agent results are posted back as **comments** on the original Linear issue, and the issue is optionally moved to a "Done" state. + +Linear channels are always **BYOB** — you provide a Linear Personal API Key. + +### Setup + +#### 1. Get a Linear Personal API Key + +1. Go to [linear.app/settings/api](https://linear.app/settings/api) +2. Click **Create key** → give it a label (e.g. "CrewForm") +3. Copy the key (`lin_api_...`) + +#### 2. Find Your Team ID + +1. In Linear, go to **Settings → Teams → [Your Team]** +2. The Team ID (UUID) is visible in the URL: `https://linear.app//settings/teams/` + +#### 3. Create the Channel in CrewForm + +1. Go to **Settings → Channels → New Channel → Linear** +2. Enter your **Personal API Key** and **Team ID** +3. Configure triggers: + - **Trigger On** — comma-separated list: `create`, `state_change`, `label` + - **Trigger States** *(optional)* — comma-separated state names, e.g. `Triage,Todo` + - **Trigger Labels** *(optional)* — comma-separated label names, e.g. `crewform,ai-task` + - **Done State** *(optional)* — move the issue to this state when the agent completes (e.g. `Done`) +4. Set a **Default Agent** or **Default Team** +5. Save — CrewForm automatically registers a webhook on your Linear team via the `linear-webhook-register` Edge Function + +### Trigger Configuration + +You can combine multiple trigger types for fine-grained control: + +| Trigger | Config | Behaviour | +|---------|--------|-----------| +| `create` | `trigger_on: create` | Agent runs on every new issue in the team | +| `state_change` | `trigger_on: state_change` + `trigger_states: Triage` | Agent runs when an issue is moved to "Triage" | +| `label` | `trigger_on: label` + `trigger_labels: crewform` | Agent runs when the "crewform" label is added | + +> **Tip:** Use `state_change` or `label` triggers to avoid processing every new issue. For example, label issues with "ai-task" to selectively trigger your agent. + +### How Issues Are Handled + +| Issue event | Behaviour | +|-------------|-----------| +| Issue created in the team | Task created (if `create` trigger is active) | +| Issue moved to a trigger state | Task created (if `state_change` trigger is active) | +| Trigger label added to an issue | Task created (if `label` trigger is active) | +| Issue already mapped to a task | Skipped (no duplicate processing) | + +When the agent completes the task: + +1. The result is posted as a **comment** on the original Linear issue +2. If a **Done State** is configured, the issue is moved to that state +3. A `linear_issue_mappings` record links the Linear issue to the CrewForm task + +### Bidirectional Flow + +Linear channels provide a full round-trip: + +``` +Issue created / state changed / label added + │ + ▼ +CrewForm creates task + issue mapping + │ + ▼ +Agent processes the prompt (issue description) + │ + ▼ +Result posted as comment on issue + │ + ▼ +Issue moved to "Done" state (optional) +``` + +### Priority Mapping + +Linear issue priorities are mapped to CrewForm task priorities: + +| Linear Priority | CrewForm Priority | +|----------------|-------------------| +| No priority (0) | Low | +| Urgent (1) | Urgent | +| High (2) | High | +| Medium (3) | Medium | +| Low (4) | Low | + +### Webhook Management + +When you create a Linear channel, CrewForm automatically registers a webhook on your Linear team via the Linear GraphQL API. The webhook secret is stored in the channel config for HMAC signature verification. + +If you need to manually manage webhooks, use the Linear API: + +```bash +# List webhooks +curl -X POST https://api.linear.app/graphql \ + -H "Authorization: lin_api_..." \ + -H "Content-Type: application/json" \ + -d '{"query": "{ webhooks { nodes { id url enabled } } }"}' +``` + +### Required Environment Variables (Self-Hosted) + +No additional environment variables are needed — the Linear API Key is stored per-channel in the database. + +--- + ## Message Log All inbound and outbound channel messages are logged. View them in **Settings → Channels → [Channel Name] → Message Log**: diff --git a/src/components/settings/MessagingChannelsSettings.tsx b/src/components/settings/MessagingChannelsSettings.tsx index 988373a8..7145d492 100644 --- a/src/components/settings/MessagingChannelsSettings.tsx +++ b/src/components/settings/MessagingChannelsSettings.tsx @@ -204,7 +204,9 @@ function CreateChannelForm({ workspaceId, onClose }: { workspaceId: string; onCl const [isManaged, setIsManaged] = useState(true) const [routeType, setRouteType] = useState<'agent' | 'team'>('agent') const [selectedId, setSelectedId] = useState('') + const [linearStatus, setLinearStatus] = useState(null) const createChannel = useCreateChannel() + const updateChannel = useUpdateChannel(workspaceId) const { agents } = useAgents(workspaceId) const { teams } = useTeams(workspaceId) @@ -242,7 +244,51 @@ function CreateChannelForm({ workspaceId, onClose }: { workspaceId: string; onCl default_team_id: routeType === 'team' ? selectedId : undefined, } createChannel.mutate(input, { - onSuccess: () => { onClose() }, + onSuccess: async (channel) => { + // For Linear channels, auto-register the webhook + if (isLinear && config.api_key && config.linear_team_id) { + setLinearStatus('Registering Linear webhook…') + try { + const { supabase } = await import('@/lib/supabase') + const { data, error } = await supabase.functions.invoke('linear-webhook-register', { + body: { + api_key: config.api_key, + team_id: config.linear_team_id, + }, + }) + + if (error) { + console.error('[Linear] Webhook registration failed:', error.message) + setLinearStatus('⚠ Webhook registration failed — you can set it up manually in Linear.') + setTimeout(onClose, 3000) + return + } + + const result = data as { webhook_id: string; webhook_secret: string; callback_url: string } + + // Update channel config with webhook secret + updateChannel.mutate({ + id: channel.id, + input: { + config: { + ...resolvedConfig, + webhook_secret: result.webhook_secret, + webhook_id: result.webhook_id, + }, + }, + }) + + setLinearStatus('✓ Webhook registered — Linear issues will now trigger your agent.') + setTimeout(onClose, 2000) + } catch (err) { + console.error('[Linear] Webhook registration error:', err) + setLinearStatus('⚠ Channel created but webhook registration failed. Set up the webhook manually.') + setTimeout(onClose, 3000) + } + } else { + onClose() + } + }, }) } @@ -422,6 +468,21 @@ function CreateChannelForm({ workspaceId, onClose }: { workspaceId: string; onCl )} + {/* Linear webhook registration status */} + {linearStatus && ( +
+ {linearStatus.includes('Registering') && } + {linearStatus} +
+ )} + {/* Actions */}
From 54c200f236d196ad58d55eb6afb66f4dd852c6b3 Mon Sep 17 00:00:00 2001 From: Vincent Grobler Date: Fri, 15 May 2026 14:04:36 +0100 Subject: [PATCH 2/2] fix: resolve eslint errors in Linear webhook registration - Wrap async onSuccess in void IIFE (no-misused-promises) - Type invoke response explicitly (no-unsafe-assignment) - Safe access to error.message (no-unsafe-member-access) --- .../settings/MessagingChannelsSettings.tsx | 76 ++++++++++--------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/src/components/settings/MessagingChannelsSettings.tsx b/src/components/settings/MessagingChannelsSettings.tsx index 7145d492..f0d83366 100644 --- a/src/components/settings/MessagingChannelsSettings.tsx +++ b/src/components/settings/MessagingChannelsSettings.tsx @@ -244,47 +244,51 @@ function CreateChannelForm({ workspaceId, onClose }: { workspaceId: string; onCl default_team_id: routeType === 'team' ? selectedId : undefined, } createChannel.mutate(input, { - onSuccess: async (channel) => { + onSuccess: (channel) => { // For Linear channels, auto-register the webhook if (isLinear && config.api_key && config.linear_team_id) { setLinearStatus('Registering Linear webhook…') - try { - const { supabase } = await import('@/lib/supabase') - const { data, error } = await supabase.functions.invoke('linear-webhook-register', { - body: { - api_key: config.api_key, - team_id: config.linear_team_id, - }, - }) - - if (error) { - console.error('[Linear] Webhook registration failed:', error.message) - setLinearStatus('⚠ Webhook registration failed — you can set it up manually in Linear.') + void (async () => { + try { + const { supabase } = await import('@/lib/supabase') + const resp: { data: { webhook_id: string; webhook_secret: string; callback_url: string } | null; error: { message: string } | null } = + await supabase.functions.invoke('linear-webhook-register', { + body: { + api_key: config.api_key, + team_id: config.linear_team_id, + }, + }) + + if (resp.error) { + console.error('[Linear] Webhook registration failed:', resp.error.message) + setLinearStatus('⚠ Webhook registration failed — you can set it up manually in Linear.') + setTimeout(onClose, 3000) + return + } + + const result = resp.data + if (result) { + // Update channel config with webhook secret + updateChannel.mutate({ + id: channel.id, + input: { + config: { + ...resolvedConfig, + webhook_secret: result.webhook_secret, + webhook_id: result.webhook_id, + }, + }, + }) + } + + setLinearStatus('✓ Webhook registered — Linear issues will now trigger your agent.') + setTimeout(onClose, 2000) + } catch (err) { + console.error('[Linear] Webhook registration error:', err) + setLinearStatus('⚠ Channel created but webhook registration failed. Set up the webhook manually.') setTimeout(onClose, 3000) - return } - - const result = data as { webhook_id: string; webhook_secret: string; callback_url: string } - - // Update channel config with webhook secret - updateChannel.mutate({ - id: channel.id, - input: { - config: { - ...resolvedConfig, - webhook_secret: result.webhook_secret, - webhook_id: result.webhook_id, - }, - }, - }) - - setLinearStatus('✓ Webhook registered — Linear issues will now trigger your agent.') - setTimeout(onClose, 2000) - } catch (err) { - console.error('[Linear] Webhook registration error:', err) - setLinearStatus('⚠ Channel created but webhook registration failed. Set up the webhook manually.') - setTimeout(onClose, 3000) - } + })() } else { onClose() }