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.slugequalsquestions - Action:
award_credits(5 credits, "Asked a question")
Route bug reports automatically
- Trigger:
forum.thread.created - Condition:
data.thread.titlestarts_with[BUG] - Action:
move_threadto forumbugs
DM a member their first time someone replies to them
- Trigger:
forum.reply.posted - Condition:
data.author.idnot_equals{{data.thread.author_id}}(someone other than the OP replied) - Action:
send_dmto{{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_emptyfor defensive guards. If a trigger's payload changes shape, conditions that depend on a specific field can quietly stop firing. Pair them withis_setchecks 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.*