feat(automations): no-code workflow automation builder (LC-495) #493

Merged
longjacksonle merged 6 commits from feat/LC-495-workflow-automations into main 2026-06-29 19:40:18 +02:00

LC-495: Workflow automation builder (triggers -> actions)

No-code, room-scoped automation rules: "when X happens in this room, do Y." This is the Slack Workflow Builder core - welcome-style auto-posts and keyword auto-responders - built as a clean vertical slice over the existing event hooks and message-post pipeline.

What a room manager can build

On the room Manage page there is a new Automations card. Each rule is: a trigger (with an optional match filter) -> an action.

  • Triggers: a message contains (case-insensitive substring; blank = any message) and someone reacts with (blank = any emoji).
  • Action: post a message as the automation bot. The body is a template supporting {user} (triggering user), {text} (the message body) and {emoji} (the reaction).
  • Rules can be enabled/disabled and deleted; the list updates in place over HTMX (no reload).

Examples: "when a message contains help -> post Need a hand? Ask in #support." or "when someone reacts with 🎉 -> post {user} is celebrating!".

How it fits the codebase

  • Persistence (db::automations, migration 0079_room_automations.sql): a room-scoped rule table. trigger_kind/action_kind are open TEXT, not CHECK enums, so new triggers/actions are a one-file migration plus an engine branch - no table rewrite. Toggle/delete are room-scoped so a forged id from another room is a no-op.
  • Engine (automations.rs): pure match + template helpers (unit-tested) plus on_message_posted / on_reaction_added. Hooked in off the request path (spawned, like the coyote / voice-transcription tasks) so a matched rule never slows the sender.
  • No loop/cascade risk: the engine is invoked ONLY from the human post/reaction handlers, never from finalize_message_send, and it ignores bot authors - so an action message cannot re-trigger automations. Bounded to 5 actions per event; per-room cap of 25 rules; action body honors the LC-153 markdown length bound.
  • UI (routes::automations + partials/room_automations.html): require_can_manage-gated CRUD; the shared partial is included by the manage page and rendered standalone by the handlers (the shared-partial convention). Hidden for DMs. en + es strings added (catalog parity test passes).

Scope / follow-ups

v1 deliberately ships two triggers and one action as a complete slice. The ticket also lists webhook / notify / add-label actions and more triggers (member joined, etc.); those are natural follow-ons - the *_kind columns and the engine's single match-set make them additive. Note: "member joined" maps poorly onto this app's open-room enclave model (public rooms have no per-room join event), so it was left out rather than half-built.

Tests

just check (clippy standalone + saas + desktop + fmt), just test, and just test-saas all pass. New: db_automations.rs (CRUD + active-trigger filter + room-scoping) and engine unit tests (filter matching, template substitution, known-kind validation).

Not operator-visible: no new env var or config; no [operator-action] marker.

## LC-495: Workflow automation builder (triggers -> actions) No-code, room-scoped automation rules: "when X happens in this room, do Y." This is the Slack Workflow Builder core - welcome-style auto-posts and keyword auto-responders - built as a clean vertical slice over the existing event hooks and message-post pipeline. ### What a room manager can build On the room **Manage** page there is a new **Automations** card. Each rule is: a trigger (with an optional match filter) -> an action. - Triggers: **a message contains <keyword>** (case-insensitive substring; blank = any message) and **someone reacts with <emoji>** (blank = any emoji). - Action: **post a message** as the `automation` bot. The body is a template supporting `{user}` (triggering user), `{text}` (the message body) and `{emoji}` (the reaction). - Rules can be enabled/disabled and deleted; the list updates in place over HTMX (no reload). Examples: "when a message contains `help` -> post `Need a hand? Ask in #support.`" or "when someone reacts with 🎉 -> post `{user} is celebrating!`". ### How it fits the codebase - **Persistence** (`db::automations`, migration `0079_room_automations.sql`): a room-scoped rule table. `trigger_kind`/`action_kind` are open TEXT, not CHECK enums, so new triggers/actions are a one-file migration plus an engine branch - no table rewrite. Toggle/delete are room-scoped so a forged id from another room is a no-op. - **Engine** (`automations.rs`): pure match + template helpers (unit-tested) plus `on_message_posted` / `on_reaction_added`. Hooked in off the request path (spawned, like the coyote / voice-transcription tasks) so a matched rule never slows the sender. - **No loop/cascade risk:** the engine is invoked ONLY from the human post/reaction handlers, never from `finalize_message_send`, and it ignores bot authors - so an action message cannot re-trigger automations. Bounded to 5 actions per event; per-room cap of 25 rules; action body honors the LC-153 markdown length bound. - **UI** (`routes::automations` + `partials/room_automations.html`): require_can_manage-gated CRUD; the shared partial is included by the manage page and rendered standalone by the handlers (the shared-partial convention). Hidden for DMs. en + es strings added (catalog parity test passes). ### Scope / follow-ups v1 deliberately ships two triggers and one action as a complete slice. The ticket also lists webhook / notify / add-label actions and more triggers (member joined, etc.); those are natural follow-ons - the `*_kind` columns and the engine's single match-set make them additive. Note: "member joined" maps poorly onto this app's open-room enclave model (public rooms have no per-room join event), so it was left out rather than half-built. ### Tests `just check` (clippy standalone + saas + desktop + fmt), `just test`, and `just test-saas` all pass. New: `db_automations.rs` (CRUD + active-trigger filter + room-scoping) and engine unit tests (filter matching, template substitution, known-kind validation). Not operator-visible: no new env var or config; no `[operator-action]` marker.
Persistence for the workflow-automation builder: a room-scoped "when X, do Y" rule table plus its CRUD/query layer. trigger_kind and action_kind are open TEXT (not CHECK enums) so future triggers/actions slot in without a schema change; the engine validates known kinds. Queries are room-scoped on toggle/delete so a forged id from another room is a no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The runtime half: match a trigger's filter (case-insensitive keyword substring for messages, emoji match for reactions; blank = any) and run the post_message action as a lazily-created `automation` bot, with {user}/{text}/{emoji} template substitution. Bounded to 5 actions per event. No cascade/loop risk: the engine is invoked only from the human post/reaction handlers, never from finalize_message_send, and it ignores bot authors. Pure match/template helpers are unit-tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hook the engine in off the request path (spawned, like coyote/voice transcription) so a matched rule never slows the sender. message_posted fires after finalize in post_message (quarantined/slash-handled posts already returned, so they are excluded); reaction_added fires on additions only in toggle_reaction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An "Automations" card on the room manage page: list rules with enable/disable + delete, and a create form (trigger, optional match, action body). All three mutations are require_can_manage-gated and return the re-rendered #lc-automations section so HTMX swaps it in place; the same partial is included by the page and rendered standalone by the handlers (shared-partial convention). Hidden for DMs. Per-room rule cap of 25; action body honors the LC-153 message length bound.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test(automations): persistence + room-scoping coverage (LC-495)
All checks were successful
check-secrets / Nosey parker (push) Successful in 6s
check-secrets / Kingfisher (push) Successful in 9s
check-secrets / TruffleHog (push) Successful in 6s
check-secrets / Nosey parker (pull_request) Successful in 6s
Check / clippy + fmt + tests (pull_request) Successful in 3m28s
Create release / Create release from merged PR (pull_request) Has been skipped
check-secrets / TruffleHog (pull_request) Successful in 4s
check-secrets / Kingfisher (pull_request) Successful in 6s
d23c7d5e29
Covers insert/list, the active-trigger filter excluding disabled and other-room rules, and that toggle/delete are no-ops against the wrong room id.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle scheduled this pull request to auto merge when all checks succeed 2026-06-29 19:37:48 +02:00
longjacksonle deleted branch feat/LC-495-workflow-automations 2026-06-29 19:40:18 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!493
No description provided.