Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a90bed0
feat(slack): trigger automations from watched channel messages
ColeMurray Jun 24, 2026
6f28d52
fix(slack): respect reply_in_thread and harden automation inputs
ColeMurray Jun 24, 2026
050afc2
refactor(slack): store reply_in_thread in trigger_config
ColeMurray Jun 24, 2026
7b59e8f
refactor(slack): extract SlackChannelStore from AutomationStore
ColeMurray Jun 24, 2026
fb2350a
Merge origin/main into feat/slack-channel-triggers
ColeMurray Jun 24, 2026
bb5cb88
refactor(slack): store run coordinates in trigger_run_metadata
ColeMurray Jun 24, 2026
e438482
refactor(slack): move replyInThread into trigger_config
ColeMurray Jun 24, 2026
33c640f
refactor(slack): drop CacheSlot abstraction, KV-back watched channels
ColeMurray Jun 25, 2026
3459eb1
fix(slack): harden trigger-config validation before persistence
ColeMurray Jun 25, 2026
e839c83
refactor(slack): drop automation-complete message and replyInThread
ColeMurray Jun 25, 2026
c4f55c0
refactor(slack): remove the max-runs-per-hour cap
ColeMurray Jun 25, 2026
40e7cf9
feat(slack): allow slack_event automations without a text filter
ColeMurray Jun 25, 2026
32b10df
feat(slack): select channels by name in automation setup
ColeMurray Jun 25, 2026
15f0bfb
fix(slack): show channel names and readable text_match on automation …
ColeMurray Jun 25, 2026
d2f2183
docs(slack): align automation docs with slack-trigger changes
ColeMurray Jun 25, 2026
272a6d4
fix(slack): stop forwarding automation tool-calls to the scheduler
ColeMurray Jun 25, 2026
e905238
feat(slack): post the agent result to Slack on automation completion
ColeMurray Jun 25, 2026
7dd1522
docs(slack): document the automation result posted on completion
ColeMurray Jun 25, 2026
9a7b7a0
feat(slack): steer thread follow-ups into the active run's session
ColeMurray Jun 25, 2026
cbbac17
feat(slack): continue thread session on replies after run completes
ColeMurray Jun 26, 2026
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
82 changes: 70 additions & 12 deletions docs/AUTOMATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ session whenever the trigger fires.

Trigger types:

| Trigger Type | Description | Availability |
| ------------------- | ----------------------------------------- | ------------ |
| **Schedule** | Run on a cron schedule | Available |
| **Inbound Webhook** | Trigger from any system with an HTTP POST | Available |
| **Sentry Alert** | Trigger from a Sentry Custom Integration | Available |
| **GitHub Event** | Trigger on GitHub activity | Planned |
| **Linear Event** | Trigger on Linear activity | Planned |
| Trigger Type | Description | Availability |
| ------------------- | ----------------------------------------- | ------------------ |
| **Schedule** | Run on a cron schedule | Available |
| **Inbound Webhook** | Trigger from any system with an HTTP POST | Available |
| **Sentry Alert** | Trigger from a Sentry Custom Integration | Available |
| **Slack Message** | Trigger on messages in watched channels | Available (opt-in) |
| **GitHub Event** | Trigger on GitHub activity | Planned |
| **Linear Event** | Trigger on Linear activity | Planned |

Common use cases include nightly dependency updates, reacting to deploy or incident events, triaging
new Sentry issues, and recurring report generation.
Expand Down Expand Up @@ -45,11 +46,12 @@ Start by choosing a **Trigger Type**. The rest of the form adjusts based on that

### Trigger-Specific Fields

| Trigger Type | Additional Fields |
| ------------------- | ------------------------------------------- |
| **Schedule** | **Schedule** and **Timezone** |
| **Inbound Webhook** | No extra required fields |
| **Sentry Alert** | **Event Type** and **Sentry Client Secret** |
| Trigger Type | Additional Fields |
| ------------------- | -------------------------------------------------------------------------------------------- |
| **Schedule** | **Schedule** and **Timezone** |
| **Inbound Webhook** | No extra required fields |
| **Sentry Alert** | **Event Type** and **Sentry Client Secret** |
| **Slack Message** | **Conditions** (a Slack Channel condition is required; a Message Text condition is optional) |

For non-schedule automations, schedule fields are not used.

Expand Down Expand Up @@ -192,6 +194,57 @@ concurrency protection.

---

## Slack Message Triggers

A **Slack Message** automation starts a session when someone posts a matching message in a watched
Slack channel. Unlike `@mention` sessions (which are explicit, interactive requests), these triggers
fire on ambient channel messages that match the conditions you define.

This source is opt-in per deployment and ships **disabled by default**. Enabling it requires the
operator to set the `SLACK_TRIGGERS_ENABLED` flag and configure the Slack app — see
[the Slack integration guide](integrations/SLACK.md#channel-message-triggers) for setup and the
threat model. The web form and these conditions are always available to author; messages are only
ingested once the flag is on.

### Conditions

A Slack automation must define at least a **Slack Channel** condition; the rest are optional
filters.

- **Slack Channel** (required) — the channels to watch. Pick channels by name in the web form;
channel IDs (for example `C0123ABCD`) also work as a fallback when channel listing is unavailable.
Only messages in these channels are considered, and the bot must be a member of each.
- **Message Text** (optional) — filter on the message text. Without it, every message in the watched
channels triggers the automation. Pick a mode:
- **contains** — the message contains the substring (optionally case-insensitive).
- **exact** — the message equals the text.
- **regex** — the message matches a regular expression. Patterns are capped in length and limited
to the `i` and `m` flags; an invalid pattern is rejected when you save.
- **Slack User** (optional) — include or exclude specific Slack user IDs (an allowlist is the
recommended way to limit who can trigger a run).

A message runs the automation only when **every** condition passes. The bot-mention token is
stripped before matching, and messages that `@mention` the bot are handled by the interactive
`@mention` flow instead — they never double-fire as triggers.

### Run feedback

A triggering message is marked with the 👀 reaction while its run is in flight. When the run
finishes, the agent's final response is posted as a reply in that message's thread — with links to
any pull requests it opened and to the full web session — and the reaction is cleared. A failed run
posts a short failure notice in the thread instead.

Every reply in a thread **continues the same session** — during the run and after it finishes — for
up to 24 hours after the thread's first trigger, exactly like replying in an `@mention` thread. The
reply is enqueued as a follow-up turn on that session (re-spawning it from a snapshot if it had gone
idle), and the agent posts its response in-thread when the turn finishes. A follow-up does not need
to match the trigger condition — conditions gate new runs, not replies that continue an existing
thread. If a reply races the very first trigger before its session exists, it falls back to an
ephemeral "a run is already active" notice (reason `concurrent_run_active`); a reply more than 24
hours after the first trigger starts a fresh run.

---

## Schedule Options

The schedule picker offers four presets and a custom mode:
Expand Down Expand Up @@ -304,6 +357,11 @@ Event-driven automations use concurrency keys instead. For inbound webhooks, ret
`idempotencyKey` are treated as the same event, but separate deliveries without a shared
`idempotencyKey` can overlap.

Slack Message triggers key concurrency by thread. Replies in a thread are not skipped — for 24 hours
after the thread's first trigger they continue the same session (during the run and after it
finishes), routed to that session as follow-up prompts (see the **Run feedback** note under
[Slack Message Triggers](#slack-message-triggers)).

This prevents overlapping sessions from interfering with each other on the same repository.

---
Expand Down
4 changes: 3 additions & 1 deletion docs/GETTING_STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -942,7 +942,9 @@ If the bot doesn't see the original message when tagged in a thread reply:
private channels). These are required by the `conversations.replies` API to fetch thread
messages.
2. Verify the bot has `channels:read` and `groups:read` scopes. These are required by
`conversations.info` to fetch channel name and description for context.
`conversations.info` to fetch channel name and description for context, and by
`conversations.list` to populate the automation channel picker. If the picker shows no channels,
check these scopes and that the bot is invited to the target channel.
3. If you added missing scopes, **reinstall the app** to your workspace for the new permissions to
take effect.

Expand Down
75 changes: 70 additions & 5 deletions docs/integrations/SLACK.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,13 @@ notification controls and safety notes are covered near the end.
| Set personal defaults | Use the Slack app's **Home** tab for model, reasoning effort, and branch |
| Follow the result | Read the completion reply or open the full session with **View Session** |
| Ask the agent to post Slack | Enable agent notifications, then explicitly ask the agent to post to Slack |
| Auto-trigger from a channel | Opt-in: watch a channel so matching messages start an automation |

Open-Inspect does not use slash commands today. In channels, it responds to `@mentions`, not every
message posted in the channel.
Open-Inspect does not use slash commands today. In channels, it normally responds only to
`@mentions`, not to every message. The optional
[channel-message triggers](#channel-message-triggers) feature can additionally start an
**automation** from non-mention messages that match conditions you configure; it is disabled by
default and must be enabled by an operator.

---

Expand Down Expand Up @@ -181,9 +185,9 @@ Slack thread continue the existing session.

## Optional Agent Notifications

Slack-started sessions always get their normal thread replies and completion messages. Agent
notifications are separate: they let an agent post an extra message to a Slack channel when you
explicitly ask for it:
Interactive Slack sessions (DMs and `@mentions`) always get their normal thread replies and
completion messages. Agent notifications are separate: they let an agent post an extra message to a
Slack channel when you explicitly ask for it:

```text
When you finish, post a short summary to #eng-updates.
Expand Down Expand Up @@ -220,6 +224,67 @@ always stripped from agent notification messages.

---

## Channel Message Triggers

Channel message triggers let an **automation** start a session when someone posts a matching message
in a watched channel — without `@mentioning` the bot. This is distinct from the interactive
`@mention` flow: it is driven by [automations](../AUTOMATIONS.md#slack-message-triggers) with
keyword, substring, or regex conditions.

The feature is **disabled by default** and gated by the `SLACK_TRIGGERS_ENABLED` deployment flag.
When the flag is off, the bot ignores channel messages and forwards nothing; authoring a Slack
automation in the web app is still allowed, but it will not run until the flag is enabled.

### Slack app setup

In addition to the standard event subscription the bot already uses, enable the bot to receive
ordinary channel messages:

- **Event subscriptions**: subscribe to `message.channels` (public channels). Add `message.groups`
if you also want to watch private channels.
- **Bot token scopes**: `channels:history` (public) and, for private channels, `groups:history`.
- Invite the bot to every channel you intend to watch. The bot only sees messages in channels it is
a member of.

Then, in the web app, create a **Slack Message** automation and add a **Slack Channel** condition
(pick channels by name; channel IDs also work as a fallback). Optionally add a **Message Text**
condition to filter by content. See
[Slack Message Triggers](../AUTOMATIONS.md#slack-message-triggers) for the full field reference.

### Run feedback

- A triggering message gets a 👀 reaction while its run is in flight.
- When the run finishes, the agent's final response is posted into the triggering message's thread
(with links to any pull requests and the full session), and the reaction is cleared. A failed run
posts a short failure notice instead.
- Every reply in a thread continues the same session — during the run and after it finishes — for up
to 24 hours after the thread's first trigger, like replying in an `@mention` thread. The reply is
routed to that session as a follow-up prompt (re-spawned from a snapshot if it had gone idle),
gets its own 👀 reaction and in-thread response, and does **not** need to match the trigger's text
condition — conditions gate new runs, not replies that continue a thread. A reply more than 24
hours after the first trigger starts a fresh run.

### Threat model

Channel triggers widen who can start a coding session, so weigh the following before enabling them:

- **Any member of a watched channel can trigger a run** simply by posting a matching message. Treat
every watched channel as a list of people authorized to start sessions against the automation's
repository.
- **Prefer an allowlist.** Add a **Slack User** condition (`include`) so only specific people can
trigger the automation, and keep watched channels small and trusted.
- **Message text reaches the agent.** The triggering message becomes part of the prompt. Scope the
automation's instructions defensively and rely on the deployment's repository access boundary —
the same GitHub App installation limits used elsewhere apply here too.
- **Regex conditions run untimed.** Conditions are evaluated with the native regex engine and no
per-match timeout; a pathological pattern is an operator-authored risk. Patterns are length-capped
and validated at save time, and the `SLACK_TRIGGERS_ENABLED` flag is the kill switch if a bad
pattern degrades automation dispatch.
- **The kill switch is immediate.** Setting `SLACK_TRIGGERS_ENABLED` back to `false` stops the bot
from ingesting or forwarding channel messages right away.

---

## Admin and Safety Notes

These notes are most useful for workspace admins deciding where the Slack bot should be available.
Expand Down
25 changes: 25 additions & 0 deletions packages/control-plane/src/db/automation-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { describe, it, expect, vi } from "vitest";
import {
AutomationStore,
isDuplicateKeyError,
toAutomation,
toAutomationRun,
type AutomationRow,
Expand Down Expand Up @@ -431,3 +432,27 @@ describe("AutomationStore", () => {
});
});
});

describe("isDuplicateKeyError", () => {
it("matches the trigger-key dedup index violation", () => {
expect(
isDuplicateKeyError(
new Error(
"D1_ERROR: UNIQUE constraint failed: automation_runs.automation_id, automation_runs.trigger_key"
)
)
).toBe(true);
});

it("ignores unrelated UNIQUE violations so they surface as real errors", () => {
expect(isDuplicateKeyError(new Error("UNIQUE constraint failed: automation_runs.id"))).toBe(
false
);
expect(isDuplicateKeyError(new Error("some other write failure"))).toBe(false);
});

it("handles non-Error throwables", () => {
expect(isDuplicateKeyError("UNIQUE constraint failed: x.trigger_key")).toBe(true);
expect(isDuplicateKeyError(null)).toBe(false);
});
});
Loading
Loading