mobieusKnow AI Community Manager: what it does, how to run it, how to control it History #67
Author
Patrick Bass
Submitted
May 29, 2026 7:37pm
Reviewed
May 29, 2026 7:37pm
Summary
Initial revision — automation engine reference
+ The **automation engine** lets you wire trigger events (when a new
+ member joins, when a thread is created, when a report is filed, etc.)
+ to one or more conditions and actions (send a DM, grant a badge, award
+ credits, etc.). Each rule fires when its trigger event matches and all
+ its conditions pass.
+
+ Use this page as the reference while building rules at
+ `/admin/automation/new`.
+
+ ---
+
+ ## Triggers
+
+ The trigger picks **which event fires the rule**. Every rule has exactly
+ one trigger. These are the six trigger events available today.
The **AI Community Manager** drafts member-facing messages and moderation recommendations for you to review. Nothing it produces is sent until you approve it. This page explains what it does and how to control it.
+ | Trigger | Fires when |
+ |---|---|
+ | `user.registered` | A new member joins the community |
+ | `forum.thread.created` | A new thread is created in any forum |
+ | `forum.reply.posted` | A new reply is posted to any thread |
+ | `marketplace.listing.created` | A marketplace listing is created |
+ | `report.created` | A new content report is filed |
+ | `moderation.action.taken` | After a moderator takes a moderation action |
The Community Manager is a separate system from the trigger/condition/action rules at `/admin/automation` (see [Automation rules](/know/admin-automation-rules)). This page covers the Community Manager only.
---
+
+ ## Variables (event payload paths)
+
+ When a trigger fires, the event payload is exposed as `{{data.*}}`
+ placeholders you can reference in **conditions** (the value field) and
+ **action params** (any string field). Different triggers expose
+ different fields — only use placeholders that the trigger actually
+ provides.
+
+ ### `user.registered`
+
+ | Placeholder | Type |
+ |---|---|
+ | `{{data.user.id}}` | integer |
+ | `{{data.user.username}}` | string |
+ | `{{data.user.display_name}}` | string |
+ | `{{data.user.email}}` | string (admin-only) |
+ | `{{data.user.role}}` | integer (1=user, 2=member, 3=mod, 4=admin) |
+ | `{{data.user.created_at}}` | ISO datetime |
+
+ ### `forum.thread.created`
+
+ | Placeholder | Type |
+ |---|---|
+ | `{{data.thread.id}}` | integer |
+ | `{{data.thread.title}}` | string |
+ | `{{data.thread.slug}}` | string |
+ | `{{data.thread.forum_id}}` | integer |
+ | `{{data.thread.author_id}}` | integer |
+ | `{{data.thread.created_at}}` | ISO datetime |
+ | `{{data.author.id}}` | integer |
+ | `{{data.author.username}}` | string |
+ | `{{data.author.display_name}}` | string |
+ | `{{data.forum.id}}` | integer |
+ | `{{data.forum.slug}}` | string |
+ | `{{data.forum.name}}` | string |
+
+ ### `forum.reply.posted`
+
+ | Placeholder | Type |
+ |---|---|
+ | `{{data.post.id}}` | integer |
+ | `{{data.post.thread_id}}` | integer |
+ | `{{data.post.author_id}}` | integer |
+ | `{{data.post.created_at}}` | ISO datetime |
+ | `{{data.thread.id}}` | integer |
+ | `{{data.thread.title}}` | string |
+ | `{{data.thread.forum_id}}` | integer |
+ | `{{data.author.id}}` | integer |
+ | `{{data.author.username}}` | string |
+ | `{{data.author.display_name}}` | string |
+ | `{{data.forum.id}}` | integer |
+ | `{{data.forum.slug}}` | string |
+ | `{{data.forum.name}}` | string |
+
+ ### `marketplace.listing.created`
+
+ | Placeholder | Type |
+ |---|---|
+ | `{{data.listing.id}}` | integer |
+ | `{{data.listing.title}}` | string |
+ | `{{data.listing.price_cents}}` | integer |
+ | `{{data.listing.author_id}}` | integer |
+ | `{{data.listing.created_at}}` | ISO datetime |
+ | `{{data.author.id}}` | integer |
+ | `{{data.author.username}}` | string |
+ | `{{data.author.display_name}}` | string |
+
+ ### `report.created`
+
+ | Placeholder | Type |
+ |---|---|
+ | `{{data.report.id}}` | integer |
+ | `{{data.report.target_type}}` | string (`post`, `thread`, `user`, ...) |
+ | `{{data.report.target_id}}` | integer |
+ | `{{data.report.reason}}` | string |
+ | `{{data.report.created_at}}` | ISO datetime |
+ | `{{data.reporter.id}}` | integer |
+ | `{{data.reporter.username}}` | string |
+ ### `moderation.action.taken`
## What it does
+ | Placeholder | Type |
+ |---|---|
+ | `{{data.action.id}}` | integer |
+ | `{{data.action.type}}` | string (`warn`, `mute`, `ban`, `delete`, ...) |
+ | `{{data.action.target_type}}` | string |
+ | `{{data.action.target_id}}` | integer |
+ | `{{data.action.actor_id}}` | integer |
+ | `{{data.action.created_at}}` | ISO datetime |
+ | `{{data.actor.id}}` | integer |
+ | `{{data.actor.username}}` | string |
The Community Manager watches your community for a handful of signals and drafts a response to each one. Every draft lands in an approval queue. You read it, edit it if you want, then approve or reject it. Approved drafts go out as direct messages, forum posts, or moderation actions. Rejected drafts never send.
---
+
+ ## Conditions
+
+ Conditions are **optional**. A rule with no conditions fires on every
+ trigger event. A rule with conditions only fires when **all conditions
+ pass** (logical AND across the list).
+
+ Each condition has three parts: a **field** (an event payload path
+ like `data.author.role`), an **operator**, and a **value**.
+ ### Operators
## What it drafts
+ | Operator | Meaning | Example value |
| Draft | When it's drafted | What you get |
|---|---|---|
+ | `equals` | Field equals value | `4` |
+ | `not_equals` | Field is not equal to value | `4` |
+ | `gt` | Field is greater than value | `100` |
+ | `lt` | Field is less than value | `100` |
+ | `gte` | Field is greater than or equal to value | `100` |
+ | `lte` | Field is less than or equal to value | `100` |
+ | `in` | Field is one of (comma-separated values) | `2,3,4` |
+ | `not_in` | Field is not one of (comma-separated values) | `1,2` |
+ | `contains` | Field contains substring | `bug` |
+ | `starts_with` | Field starts with prefix | `feature/` |
+ | `ends_with` | Field ends with suffix | `?` |
+ | `is_set` | Field has a value (any non-empty) | (no value) |
+ | `is_empty` | Field is empty or missing | (no value) |
+
+ ### Field paths
+
+ Field paths are written **without the `{{}}` braces** because they're
+ field selectors, not values. So in the **field** box you'd write
+ `data.author.role` (no braces). In the **value** box you'd write
+ either a literal like `4` or a placeholder like `{{data.user.id}}` to
+ compare two fields against each other.
+
+ ### Examples
| Welcome | A new member joins | A short, personal welcome message sent to the member |
| Weekly summary | Once a week | A snapshot of the past week (new members, active threads, flagged signals), posted to an admin-only forum |
| Re-engagement | A member is flagged as at risk of going quiet | A tailored message inviting them back |
| Introduction | Two members share interests and recent activity | An intro message that names the other member, sent to one of them |
| Forum reply | A thread goes unanswered and matches a known pattern | A drafted reply you can post to the thread |
| Moderation recommendation | A member files a report | A recommended action — dismiss, warn, hide, suspend, or escalate — with the reasoning |
+ | Goal | Field | Operator | Value |
+ |---|---|---|---|
+ | Only fire on admin-created threads | `data.author.role` | `equals` | `4` |
+ | Skip mod-and-above member registrations | `data.user.role` | `lt` | `3` |
+ | Only act when the report reason is "spam" | `data.report.reason` | `equals` | `spam` |
+ | Only fire on threads in specific forums | `data.forum.slug` | `in` | `announcements,off-topic` |
+ | Only act on listings priced above $50 | `data.listing.price_cents` | `gte` | `5000` |
+ | Skip threads that have no title | `data.thread.title` | `is_empty` | (leave blank) |
+ | Only fire on questions (titles ending with `?`) | `data.thread.title` | `ends_with` | `?` |
Each draft is de-duplicated, so the same event never produces two drafts for the same target.
---
+ ## Actions
## You approve everything
+ Every rule needs at least one action. Actions run in order. If any
+ action fails, subsequent actions still run (they don't short-circuit
+ each other).
The Community Manager cannot send anything on its own. A draft is delivered only when you click Approve. No schedule and no background step can send a message or take a moderation action. You are the gate, every time.
+ ### `send_dm` — send a direct message
---
+ Sends an in-app DM to the named recipient. Use `{{data.*}}` placeholders
+ in the body to personalise.
## Bring your own AI key
+ | Param | Required | Description |
+ |---|---|---|
+ | `to_user_id` | yes | Recipient user id, usually `{{data.author.id}}` |
+ | `body` | yes | Message body. Supports placeholder substitution. |
The Community Manager runs on your own Anthropic API key. You add it in Community Manager settings. Until the key is set, the Community Manager stays off: it drafts nothing and makes no API calls.
+ **Example** — DM the author when a thread they created hits the
+ `announcements` forum:
Three things must all be true for it to run:
+ ```
+ Trigger: forum.thread.created
+ Condition: data.forum.slug equals announcements
+ Action: send_dm
+ to_user_id: {{data.author.id}}
+ body: Thanks for posting "{{data.thread.title}}" — a moderator will review it shortly.
+ ```
1. The Community Manager is switched on.
2. Your plan is Pro, Creator Plus, or Sovereign.
3. Your Anthropic API key is set.
+ ### `add_badge` — grant an achievement
If any one is missing, the whole Community Manager is off, and the settings page tells you which. You pay Anthropic directly, and every draft records its token count and cost so you can see exactly what you spent.
+ Awards an achievement badge to the named user. The badge must already
+ exist in `/admin/achievements`.
---
+ | Param | Required | Description |
+ |---|---|---|
+ | `user_id` | yes | Recipient user id |
+ | `badge_slug` | yes | Achievement slug (e.g. `welcome`, `first-thread`) |
## Which plans include it
+ **Example** — grant the `welcome` badge when someone joins:
The Community Manager is available on **Pro**, **Creator Plus**, and **Sovereign** plans. Starter does not include it.
+ ```
+ Trigger: user.registered
+ Action: add_badge
+ user_id: {{data.user.id}}
+ badge_slug: welcome
+ ```
---
+ ### `award_credits` — give the user platform credits
## When drafts appear
+ Adds credits to the named user's balance. Shown in their transaction
+ history with your `reason` text.
- **Welcome**: within about a minute of a member joining.
- **Weekly summary**: once per week.
- **Re-engagement**: at-risk members are reviewed every hour.
- **Introductions**: compatible members are matched a few times a day.
- **Forum replies**: unanswered threads are scanned several times a day.
+ | Param | Required | Description |
+ |---|---|---|
+ | `user_id` | yes | Recipient user id |
+ | `amount` | yes | Integer amount (no decimals) |
+ | `reason` | yes | Reason string shown in the transaction log |
Each kind is checked on its own, and every check first confirms the Community Manager is on, so one you've switched off costs you nothing.
+ **Example** — give 10 credits for every approved marketplace listing
+ (combined with a separate moderator approval flow):
---
+ ```
+ Trigger: marketplace.listing.created
+ Action: award_credits
+ user_id: {{data.author.id}}
+ amount: 10
+ reason: Listing created
+ ```
## Cost tracking and controls
+ ### `move_thread` — move a thread to a different forum
See what the Community Manager costs at `/admin/community-manager`:
+ Re-parents a thread under the named forum slug.
- **Per draft** — every draft card shows its token count and cost.
- **30-day total** — the stat strip at the top of the queue shows your rolling spend.
- **By draft type** — spend is tracked per kind, so you can see where it goes.
+ | Param | Required | Description |
+ |---|---|---|
+ | `thread_id` | yes | Thread id to move, usually `{{data.thread.id}}` |
+ | `target_forum_slug` | yes | Slug of the destination forum |
To keep spend predictable, set a monthly budget in Community Manager settings:
+ **Example** — auto-move threads whose title starts with `[Q]` to a
+ dedicated `questions` forum:
- **Monthly token budget** — a cap on the tokens the Community Manager can use in a calendar month. A live meter shows how much of the budget you've used; it turns amber at 80% and red at 100%. Leave it blank for no cap.
- **Budget-alert email** — get an email when usage crosses 80%, and again at 100%.
- **Pause on budget hit** — when this is on and you reach the budget, the Community Manager stops drafting new messages. Drafts already in your queue stay reviewable. Drafting resumes on its own when the budget resets at the start of the next month, or as soon as you raise the cap.
+ ```
+ Trigger: forum.thread.created
+ Condition: data.thread.title starts_with [Q]
+ Action: move_thread
+ thread_id: {{data.thread.id}}
+ target_forum_slug: questions
+ ```
The budget counts every kind of Community Manager draft together.
+ ### `queue_cm_draft` — enqueue a Community Manager draft
---
+ Queues a draft for the AI Community Manager. Useful for routing
+ event-driven welcomes through admin-defined automation rules rather
+ than the default subscriber. The draft lands at `/admin/community-manager`
+ and an admin still approves it before anything sends.
## Turning kinds on and off
+ | Param | Required | Description |
+ |---|---|---|
+ | `kind` | yes | `welcome`, `weekly_summary`, `churn_reengagement`, `meet_suggestion`, or `forum_reply` |
+ | `target_user_id` | (one of) | Recipient user id, for user-targeted kinds |
+ | `target_thread_id` | (one of) | Thread id, for thread-targeted kinds |
+ | `context_json` | no | Optional extra context for the generator |
Switching the Community Manager on enables it as a whole. Six independent toggles let you run only the parts you want:
+ **Example** — fire a custom welcome path for admins who join (so admin
+ welcomes get an extra-personalised draft):
- Welcome new members
- Weekly admin summary
- Re-engagement messages
- Introductions
- Forum reply drafts
- Moderation recommendations
+ ```
+ Trigger: user.registered
+ Condition: data.user.role gte 4
+ Action: queue_cm_draft
+ kind: welcome
+ target_user_id: {{data.user.id}}
+ ```
All six start off. You'll find these toggles in two places: on the Community Manager settings page, and in Site Configuration under the AI group. Turn on any combination — welcomes without moderation, summaries without re-engagement, whatever fits. A toggle does nothing until your Anthropic key is set.
---
+
+ ## Putting it together
+
+ Rules combine **trigger + conditions + actions** to express
+ "when X happens AND Y is true, do Z". Some recipes:
+
+ **Welcome new members with a personal badge**
+ - Trigger: `user.registered`
+ - Actions: `add_badge` (welcome) + `send_dm` (a custom welcome note)
+ **Auto-credit weekly question askers**
+ - Trigger: `forum.thread.created`
+ - Condition: `data.forum.slug` equals `questions`
+ - Action: `award_credits` (5 credits, "Asked a question")
## Audit trail
+ **Route bug reports automatically**
+ - Trigger: `forum.thread.created`
+ - Condition: `data.thread.title` starts_with `[BUG]`
+ - Action: `move_thread` to forum `bugs`
Every step in a draft's life is recorded in a tamper-evident log you can review at `/admin/community-manager/audit`. Recorded steps include:
+ **DM a member their first time someone replies to them**
+ - Trigger: `forum.reply.posted`
+ - Condition: `data.author.id` not_equals `{{data.thread.author_id}}` (someone other than the OP replied)
+ - Action: `send_dm` to `{{data.thread.author_id}}` ("Your thread got its first reply")
- Drafted
- Approved
- Edited, then approved
- Rejected
- Sent
- Send failed
- Replaced by a newer draft
- Retried
---
+ ## Tips
## Where to go
+ - **Test on a quiet forum first.** Build the rule against an event you can fire on-demand (e.g. create a test thread in a hidden forum) before pointing it at a high-traffic surface.
+ - **Use `is_set` / `is_empty` for defensive guards.** If a trigger's payload changes shape, conditions that depend on a specific field can quietly stop firing. Pair them with `is_set` checks to make the dependency explicit.
+ - **Conditions are AND, not OR.** To express OR semantics, create two rules with the same trigger and action but different conditions.
+ - **Rules run independently.** Two rules on the same trigger both fire — there is no priority or short-circuit. Use distinct conditions if you want only one to fire.
+ - **Disabled rules don't run.** Toggle a rule off rather than deleting it if you want to keep the history.
+ - **Run history is at `/admin/automation/runs`** — every rule firing is logged with the event payload, the matched condition, the action result, and elapsed time.
+ - **For more advanced workflows**, the AI Community Manager (`/admin/community-manager`) drafts personalised member-facing content that goes through admin approval before sending.
- **Review and approve drafts**: `/admin/community-manager`
- **Settings** (AI key, per-kind toggles, monthly budget): `/admin/community-manager/settings`
- **Audit trail**: `/admin/community-manager/audit`
+ ---
+ *This page documents the v1 automation engine shipped on 2026-05-29.
+ The trigger list, operator list, and action list will grow over time —
+ this page will stay current.*

The automation engine lets you wire trigger events (when a new member joins, when a thread is created, when a report is filed, etc.) to one or more conditions and actions (send a DM, grant a badge, award credits, etc.). Each rule fires when its trigger event matches and all its conditions pass.

Use this page as the reference while building rules at /admin/automation/new.


Triggers

The trigger picks which event fires the rule. Every rule has exactly one trigger. These are the six trigger events available today.

Trigger Fires when
user.registered A new member joins the community
forum.thread.created A new thread is created in any forum
forum.reply.posted A new reply is posted to any thread
marketplace.listing.created A marketplace listing is created
report.created A new content report is filed
moderation.action.taken After a moderator takes a moderation action

Variables (event payload paths)

When a trigger fires, the event payload is exposed as {{data.*}} placeholders you can reference in conditions (the value field) and action params (any string field). Different triggers expose different fields — only use placeholders that the trigger actually provides.

user.registered

Placeholder Type
{{data.user.id}} integer
{{data.user.username}} string
{{data.user.display_name}} string
{{data.user.email}} string (admin-only)
{{data.user.role}} integer (1=user, 2=member, 3=mod, 4=admin)
{{data.user.created_at}} ISO datetime

forum.thread.created

Placeholder Type
{{data.thread.id}} integer
{{data.thread.title}} string
{{data.thread.slug}} string
{{data.thread.forum_id}} integer
{{data.thread.author_id}} integer
{{data.thread.created_at}} ISO datetime
{{data.author.id}} integer
{{data.author.username}} string
{{data.author.display_name}} string
{{data.forum.id}} integer
{{data.forum.slug}} string
{{data.forum.name}} string

forum.reply.posted

Placeholder Type
{{data.post.id}} integer
{{data.post.thread_id}} integer
{{data.post.author_id}} integer
{{data.post.created_at}} ISO datetime
{{data.thread.id}} integer
{{data.thread.title}} string
{{data.thread.forum_id}} integer
{{data.author.id}} integer
{{data.author.username}} string
{{data.author.display_name}} string
{{data.forum.id}} integer
{{data.forum.slug}} string
{{data.forum.name}} string

marketplace.listing.created

Placeholder Type
{{data.listing.id}} integer
{{data.listing.title}} string
{{data.listing.price_cents}} integer
{{data.listing.author_id}} integer
{{data.listing.created_at}} ISO datetime
{{data.author.id}} integer
{{data.author.username}} string
{{data.author.display_name}} string

report.created

Placeholder Type
{{data.report.id}} integer
{{data.report.target_type}} string (post, thread, user, ...)
{{data.report.target_id}} integer
{{data.report.reason}} string
{{data.report.created_at}} ISO datetime
{{data.reporter.id}} integer
{{data.reporter.username}} string

moderation.action.taken

Placeholder Type
{{data.action.id}} integer
{{data.action.type}} string (warn, mute, ban, delete, ...)
{{data.action.target_type}} string
{{data.action.target_id}} integer
{{data.action.actor_id}} integer
{{data.action.created_at}} ISO datetime
{{data.actor.id}} integer
{{data.actor.username}} string

Conditions

Conditions are optional. A rule with no conditions fires on every trigger event. A rule with conditions only fires when all conditions pass (logical AND across the list).

Each condition has three parts: a field (an event payload path like data.author.role), an operator, and a value.

Operators

Operator Meaning Example value
equals Field equals value 4
not_equals Field is not equal to value 4
gt Field is greater than value 100
lt Field is less than value 100
gte Field is greater than or equal to value 100
lte Field is less than or equal to value 100
in Field is one of (comma-separated values) 2,3,4
not_in Field is not one of (comma-separated values) 1,2
contains Field contains substring bug
starts_with Field starts with prefix feature/
ends_with Field ends with suffix ?
is_set Field has a value (any non-empty) (no value)
is_empty Field is empty or missing (no value)

Field paths

Field paths are written without the {{}} braces because they're field selectors, not values. So in the field box you'd write data.author.role (no braces). In the value box you'd write either a literal like 4 or a placeholder like {{data.user.id}} to compare two fields against each other.

Examples

Goal Field Operator Value
Only fire on admin-created threads data.author.role equals 4
Skip mod-and-above member registrations data.user.role lt 3
Only act when the report reason is "spam" data.report.reason equals spam
Only fire on threads in specific forums data.forum.slug in announcements,off-topic
Only act on listings priced above $50 data.listing.price_cents gte 5000
Skip threads that have no title data.thread.title is_empty (leave blank)
Only fire on questions (titles ending with ?) data.thread.title ends_with ?

Actions

Every rule needs at least one action. Actions run in order. If any action fails, subsequent actions still run (they don't short-circuit each other).

send_dm — send a direct message

Sends an in-app DM to the named recipient. Use {{data.*}} placeholders in the body to personalise.

Param Required Description
to_user_id yes Recipient user id, usually {{data.author.id}}
body yes Message body. Supports placeholder substitution.

Example — DM the author when a thread they created hits the announcements forum:

Trigger:    forum.thread.created
Condition:  data.forum.slug equals announcements
Action:     send_dm
  to_user_id: {{data.author.id}}
  body: Thanks for posting "{{data.thread.title}}" — a moderator will review it shortly.

add_badge — grant an achievement

Awards an achievement badge to the named user. The badge must already exist in /admin/achievements.

Param Required Description
user_id yes Recipient user id
badge_slug yes Achievement slug (e.g. welcome, first-thread)

Example — grant the welcome badge when someone joins:

Trigger:    user.registered
Action:     add_badge
  user_id: {{data.user.id}}
  badge_slug: welcome

award_credits — give the user platform credits

Adds credits to the named user's balance. Shown in their transaction history with your reason text.

Param Required Description
user_id yes Recipient user id
amount yes Integer amount (no decimals)
reason yes Reason string shown in the transaction log

Example — give 10 credits for every approved marketplace listing (combined with a separate moderator approval flow):

Trigger:    marketplace.listing.created
Action:     award_credits
  user_id: {{data.author.id}}
  amount: 10
  reason: Listing created

move_thread — move a thread to a different forum

Re-parents a thread under the named forum slug.

Param Required Description
thread_id yes Thread id to move, usually {{data.thread.id}}
target_forum_slug yes Slug of the destination forum

Example — auto-move threads whose title starts with [Q] to a dedicated questions forum:

Trigger:    forum.thread.created
Condition:  data.thread.title starts_with [Q]
Action:     move_thread
  thread_id: {{data.thread.id}}
  target_forum_slug: questions

queue_cm_draft — enqueue a Community Manager draft

Queues a draft for the AI Community Manager. Useful for routing event-driven welcomes through admin-defined automation rules rather than the default subscriber. The draft lands at /admin/community-manager and an admin still approves it before anything sends.

Param Required Description
kind yes welcome, weekly_summary, churn_reengagement, meet_suggestion, or forum_reply
target_user_id (one of) Recipient user id, for user-targeted kinds
target_thread_id (one of) Thread id, for thread-targeted kinds
context_json no Optional extra context for the generator

Example — fire a custom welcome path for admins who join (so admin welcomes get an extra-personalised draft):

Trigger:    user.registered
Condition:  data.user.role gte 4
Action:     queue_cm_draft
  kind: welcome
  target_user_id: {{data.user.id}}

Putting it together

Rules combine trigger + conditions + actions to express "when X happens AND Y is true, do Z". Some recipes:

Welcome new members with a personal badge

  • Trigger: user.registered
  • Actions: add_badge (welcome) + send_dm (a custom welcome note)

Auto-credit weekly question askers

  • Trigger: forum.thread.created
  • Condition: data.forum.slug equals questions
  • Action: award_credits (5 credits, "Asked a question")

Route bug reports automatically

  • Trigger: forum.thread.created
  • Condition: data.thread.title starts_with [BUG]
  • Action: move_thread to forum bugs

DM a member their first time someone replies to them

  • Trigger: forum.reply.posted
  • Condition: data.author.id not_equals {{data.thread.author_id}} (someone other than the OP replied)
  • Action: send_dm to {{data.thread.author_id}} ("Your thread got its first reply")

Tips

  • Test on a quiet forum first. Build the rule against an event you can fire on-demand (e.g. create a test thread in a hidden forum) before pointing it at a high-traffic surface.
  • Use is_set / is_empty for defensive guards. If a trigger's payload changes shape, conditions that depend on a specific field can quietly stop firing. Pair them with is_set checks to make the dependency explicit.
  • Conditions are AND, not OR. To express OR semantics, create two rules with the same trigger and action but different conditions.
  • Rules run independently. Two rules on the same trigger both fire — there is no priority or short-circuit. Use distinct conditions if you want only one to fire.
  • Disabled rules don't run. Toggle a rule off rather than deleting it if you want to keep the history.
  • Run history is at /admin/automation/runs — every rule firing is logged with the event payload, the matched condition, the action result, and elapsed time.
  • For more advanced workflows, the AI Community Manager (/admin/community-manager) drafts personalised member-facing content that goes through admin approval before sending.

This page documents the v1 automation engine shipped on 2026-05-29. The trigger list, operator list, and action list will grow over time — this page will stay current.

The **automation engine** lets you wire trigger events (when a new
member joins, when a thread is created, when a report is filed, etc.)
to one or more conditions and actions (send a DM, grant a badge, award
credits, etc.). Each rule fires when its trigger event matches and all
its conditions pass.

Use this page as the reference while building rules at
`/admin/automation/new`.

---

## Triggers

The trigger picks **which event fires the rule**. Every rule has exactly
one trigger. These are the six trigger events available today.

| Trigger | Fires when |
|---|---|
| `user.registered` | A new member joins the community |
| `forum.thread.created` | A new thread is created in any forum |
| `forum.reply.posted` | A new reply is posted to any thread |
| `marketplace.listing.created` | A marketplace listing is created |
| `report.created` | A new content report is filed |
| `moderation.action.taken` | After a moderator takes a moderation action |

---

## Variables (event payload paths)

When a trigger fires, the event payload is exposed as `{{data.*}}`
placeholders you can reference in **conditions** (the value field) and
**action params** (any string field). Different triggers expose
different fields — only use placeholders that the trigger actually
provides.

### `user.registered`

| Placeholder | Type |
|---|---|
| `{{data.user.id}}` | integer |
| `{{data.user.username}}` | string |
| `{{data.user.display_name}}` | string |
| `{{data.user.email}}` | string (admin-only) |
| `{{data.user.role}}` | integer (1=user, 2=member, 3=mod, 4=admin) |
| `{{data.user.created_at}}` | ISO datetime |

### `forum.thread.created`

| Placeholder | Type |
|---|---|
| `{{data.thread.id}}` | integer |
| `{{data.thread.title}}` | string |
| `{{data.thread.slug}}` | string |
| `{{data.thread.forum_id}}` | integer |
| `{{data.thread.author_id}}` | integer |
| `{{data.thread.created_at}}` | ISO datetime |
| `{{data.author.id}}` | integer |
| `{{data.author.username}}` | string |
| `{{data.author.display_name}}` | string |
| `{{data.forum.id}}` | integer |
| `{{data.forum.slug}}` | string |
| `{{data.forum.name}}` | string |

### `forum.reply.posted`

| Placeholder | Type |
|---|---|
| `{{data.post.id}}` | integer |
| `{{data.post.thread_id}}` | integer |
| `{{data.post.author_id}}` | integer |
| `{{data.post.created_at}}` | ISO datetime |
| `{{data.thread.id}}` | integer |
| `{{data.thread.title}}` | string |
| `{{data.thread.forum_id}}` | integer |
| `{{data.author.id}}` | integer |
| `{{data.author.username}}` | string |
| `{{data.author.display_name}}` | string |
| `{{data.forum.id}}` | integer |
| `{{data.forum.slug}}` | string |
| `{{data.forum.name}}` | string |

### `marketplace.listing.created`

| Placeholder | Type |
|---|---|
| `{{data.listing.id}}` | integer |
| `{{data.listing.title}}` | string |
| `{{data.listing.price_cents}}` | integer |
| `{{data.listing.author_id}}` | integer |
| `{{data.listing.created_at}}` | ISO datetime |
| `{{data.author.id}}` | integer |
| `{{data.author.username}}` | string |
| `{{data.author.display_name}}` | string |

### `report.created`

| Placeholder | Type |
|---|---|
| `{{data.report.id}}` | integer |
| `{{data.report.target_type}}` | string (`post`, `thread`, `user`, ...) |
| `{{data.report.target_id}}` | integer |
| `{{data.report.reason}}` | string |
| `{{data.report.created_at}}` | ISO datetime |
| `{{data.reporter.id}}` | integer |
| `{{data.reporter.username}}` | string |

### `moderation.action.taken`

| Placeholder | Type |
|---|---|
| `{{data.action.id}}` | integer |
| `{{data.action.type}}` | string (`warn`, `mute`, `ban`, `delete`, ...) |
| `{{data.action.target_type}}` | string |
| `{{data.action.target_id}}` | integer |
| `{{data.action.actor_id}}` | integer |
| `{{data.action.created_at}}` | ISO datetime |
| `{{data.actor.id}}` | integer |
| `{{data.actor.username}}` | string |

---

## Conditions

Conditions are **optional**. A rule with no conditions fires on every
trigger event. A rule with conditions only fires when **all conditions
pass** (logical AND across the list).

Each condition has three parts: a **field** (an event payload path
like `data.author.role`), an **operator**, and a **value**.

### Operators

| Operator | Meaning | Example value |
|---|---|---|
| `equals` | Field equals value | `4` |
| `not_equals` | Field is not equal to value | `4` |
| `gt` | Field is greater than value | `100` |
| `lt` | Field is less than value | `100` |
| `gte` | Field is greater than or equal to value | `100` |
| `lte` | Field is less than or equal to value | `100` |
| `in` | Field is one of (comma-separated values) | `2,3,4` |
| `not_in` | Field is not one of (comma-separated values) | `1,2` |
| `contains` | Field contains substring | `bug` |
| `starts_with` | Field starts with prefix | `feature/` |
| `ends_with` | Field ends with suffix | `?` |
| `is_set` | Field has a value (any non-empty) | (no value) |
| `is_empty` | Field is empty or missing | (no value) |

### Field paths

Field paths are written **without the `{{}}` braces** because they're
field selectors, not values. So in the **field** box you'd write
`data.author.role` (no braces). In the **value** box you'd write
either a literal like `4` or a placeholder like `{{data.user.id}}` to
compare two fields against each other.

### Examples

| Goal | Field | Operator | Value |
|---|---|---|---|
| Only fire on admin-created threads | `data.author.role` | `equals` | `4` |
| Skip mod-and-above member registrations | `data.user.role` | `lt` | `3` |
| Only act when the report reason is "spam" | `data.report.reason` | `equals` | `spam` |
| Only fire on threads in specific forums | `data.forum.slug` | `in` | `announcements,off-topic` |
| Only act on listings priced above $50 | `data.listing.price_cents` | `gte` | `5000` |
| Skip threads that have no title | `data.thread.title` | `is_empty` | (leave blank) |
| Only fire on questions (titles ending with `?`) | `data.thread.title` | `ends_with` | `?` |

---

## Actions

Every rule needs at least one action. Actions run in order. If any
action fails, subsequent actions still run (they don't short-circuit
each other).

### `send_dm` — send a direct message

Sends an in-app DM to the named recipient. Use `{{data.*}}` placeholders
in the body to personalise.

| Param | Required | Description |
|---|---|---|
| `to_user_id` | yes | Recipient user id, usually `{{data.author.id}}` |
| `body` | yes | Message body. Supports placeholder substitution. |

**Example** — DM the author when a thread they created hits the
`announcements` forum:

```
Trigger:    forum.thread.created
Condition:  data.forum.slug equals announcements
Action:     send_dm
  to_user_id: {{data.author.id}}
  body: Thanks for posting "{{data.thread.title}}" — a moderator will review it shortly.
```

### `add_badge` — grant an achievement

Awards an achievement badge to the named user. The badge must already
exist in `/admin/achievements`.

| Param | Required | Description |
|---|---|---|
| `user_id` | yes | Recipient user id |
| `badge_slug` | yes | Achievement slug (e.g. `welcome`, `first-thread`) |

**Example** — grant the `welcome` badge when someone joins:

```
Trigger:    user.registered
Action:     add_badge
  user_id: {{data.user.id}}
  badge_slug: welcome
```

### `award_credits` — give the user platform credits

Adds credits to the named user's balance. Shown in their transaction
history with your `reason` text.

| Param | Required | Description |
|---|---|---|
| `user_id` | yes | Recipient user id |
| `amount` | yes | Integer amount (no decimals) |
| `reason` | yes | Reason string shown in the transaction log |

**Example** — give 10 credits for every approved marketplace listing
(combined with a separate moderator approval flow):

```
Trigger:    marketplace.listing.created
Action:     award_credits
  user_id: {{data.author.id}}
  amount: 10
  reason: Listing created
```

### `move_thread` — move a thread to a different forum

Re-parents a thread under the named forum slug.

| Param | Required | Description |
|---|---|---|
| `thread_id` | yes | Thread id to move, usually `{{data.thread.id}}` |
| `target_forum_slug` | yes | Slug of the destination forum |

**Example** — auto-move threads whose title starts with `[Q]` to a
dedicated `questions` forum:

```
Trigger:    forum.thread.created
Condition:  data.thread.title starts_with [Q]
Action:     move_thread
  thread_id: {{data.thread.id}}
  target_forum_slug: questions
```

### `queue_cm_draft` — enqueue a Community Manager draft

Queues a draft for the AI Community Manager. Useful for routing
event-driven welcomes through admin-defined automation rules rather
than the default subscriber. The draft lands at `/admin/community-manager`
and an admin still approves it before anything sends.

| Param | Required | Description |
|---|---|---|
| `kind` | yes | `welcome`, `weekly_summary`, `churn_reengagement`, `meet_suggestion`, or `forum_reply` |
| `target_user_id` | (one of) | Recipient user id, for user-targeted kinds |
| `target_thread_id` | (one of) | Thread id, for thread-targeted kinds |
| `context_json` | no | Optional extra context for the generator |

**Example** — fire a custom welcome path for admins who join (so admin
welcomes get an extra-personalised draft):

```
Trigger:    user.registered
Condition:  data.user.role gte 4
Action:     queue_cm_draft
  kind: welcome
  target_user_id: {{data.user.id}}
```

---

## Putting it together

Rules combine **trigger + conditions + actions** to express
"when X happens AND Y is true, do Z". Some recipes:

**Welcome new members with a personal badge**
- Trigger: `user.registered`
- Actions: `add_badge` (welcome) + `send_dm` (a custom welcome note)

**Auto-credit weekly question askers**
- Trigger: `forum.thread.created`
- Condition: `data.forum.slug` equals `questions`
- Action: `award_credits` (5 credits, "Asked a question")

**Route bug reports automatically**
- Trigger: `forum.thread.created`
- Condition: `data.thread.title` starts_with `[BUG]`
- Action: `move_thread` to forum `bugs`

**DM a member their first time someone replies to them**
- Trigger: `forum.reply.posted`
- Condition: `data.author.id` not_equals `{{data.thread.author_id}}` (someone other than the OP replied)
- Action: `send_dm` to `{{data.thread.author_id}}` ("Your thread got its first reply")

---

## Tips

- **Test on a quiet forum first.** Build the rule against an event you can fire on-demand (e.g. create a test thread in a hidden forum) before pointing it at a high-traffic surface.
- **Use `is_set` / `is_empty` for defensive guards.** If a trigger's payload changes shape, conditions that depend on a specific field can quietly stop firing. Pair them with `is_set` checks to make the dependency explicit.
- **Conditions are AND, not OR.** To express OR semantics, create two rules with the same trigger and action but different conditions.
- **Rules run independently.** Two rules on the same trigger both fire — there is no priority or short-circuit. Use distinct conditions if you want only one to fire.
- **Disabled rules don't run.** Toggle a rule off rather than deleting it if you want to keep the history.
- **Run history is at `/admin/automation/runs`** — every rule firing is logged with the event payload, the matched condition, the action result, and elapsed time.
- **For more advanced workflows**, the AI Community Manager (`/admin/community-manager`) drafts personalised member-facing content that goes through admin approval before sending.

---

*This page documents the v1 automation engine shipped on 2026-05-29.
The trigger list, operator list, and action list will grow over time —
this page will stay current.*