Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 112 additions & 1 deletion docs/channels.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

---

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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/<workspace>/settings/teams/<TEAM_ID>`

#### 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**:
Expand Down
71 changes: 68 additions & 3 deletions src/components/settings/MessagingChannelsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(null)
const createChannel = useCreateChannel()
const updateChannel = useUpdateChannel(workspaceId)
const { agents } = useAgents(workspaceId)
const { teams } = useTeams(workspaceId)

Expand Down Expand Up @@ -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()
}
},
})
}

Expand Down Expand Up @@ -422,6 +472,21 @@ function CreateChannelForm({ workspaceId, onClose }: { workspaceId: string; onCl
</a>
)}

{/* Linear webhook registration status */}
{linearStatus && (
<div className={cn(
'rounded-lg border p-3 text-xs',
linearStatus.startsWith('✓')
? 'border-emerald-700 bg-emerald-500/10 text-emerald-400'
: linearStatus.startsWith('⚠')
? 'border-amber-700 bg-amber-500/10 text-amber-400'
: 'border-gray-700 bg-gray-900/50 text-gray-400',
)}>
{linearStatus.includes('Registering') && <Loader2 className="mr-1.5 inline-block h-3 w-3 animate-spin" />}
{linearStatus}
</div>
)}

{/* Actions */}
<div className="flex items-center justify-end gap-2 pt-2">
<button
Expand All @@ -433,10 +498,10 @@ function CreateChannelForm({ workspaceId, onClose }: { workspaceId: string; onCl
</button>
<button
type="submit"
disabled={createChannel.isPending}
disabled={createChannel.isPending || !!linearStatus}
className="flex items-center gap-2 rounded-lg bg-brand-primary px-4 py-1.5 text-sm font-medium text-black hover:bg-brand-primary/90 disabled:opacity-50"
>
{createChannel.isPending && <Loader2 className="h-3 w-3 animate-spin" />}
{(createChannel.isPending || linearStatus?.includes('Registering')) && <Loader2 className="h-3 w-3 animate-spin" />}
Create Channel
</button>
</div>
Expand Down
Loading