diff --git a/docs/channels.md b/docs/channels.md index 323a34c..00dbe69 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 988373a..f0d8336 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,55 @@ function CreateChannelForm({ workspaceId, onClose }: { workspaceId: string; onCl default_team_id: routeType === 'team' ? selectedId : undefined, } createChannel.mutate(input, { - onSuccess: () => { onClose() }, + onSuccess: (channel) => { + // For Linear channels, auto-register the webhook + if (isLinear && config.api_key && config.linear_team_id) { + setLinearStatus('Registering Linear webhook…') + 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) + } + })() + } else { + onClose() + } + }, }) } @@ -422,6 +472,21 @@ function CreateChannelForm({ workspaceId, onClose }: { workspaceId: string; onCl )} + {/* Linear webhook registration status */} + {linearStatus && ( +
+ {linearStatus.includes('Registering') && } + {linearStatus} +
+ )} + {/* Actions */}