Skip to main content
The Automations engine turns events into actions. Each rule has a trigger (one of 15 event types), zero or more filters / payload-match conditions, and one or more ordered actions. Rules are tenant-scoped, run on Celery, and every run is logged for replay + audit.
Use dispatch(trigger_key, payload) from any backend code to fire a trigger. The engine matches it against user-defined rules and enqueues the action chain — your code doesn’t need to know which rules exist.

Building a rule

In the UI, an automation rule has four sections:
  1. Trigger — pick from 15 trigger types. The form re-renders to show the trigger’s config schema (e.g., note.status_changed shows “from status” / “to status” pickers; schedule.weekly shows day-of-week selectors).
  2. Filters — additional payload-match conditions. The trigger may already filter (e.g., note.created can pre-filter by category); filters here are arbitrary expressions on the payload.
  3. Actions — one or more, ordered. Each action has its own config schema. Actions can reference any field on the trigger payload via Jinja-style templates: {{ payload.note_id }}.
  4. Settings — rate limit (e.g., max once per hour per client_id), dedup key, enabled/disabled, owner.
Saving the rule writes an Automation row plus (for schedule triggers) a corresponding django_celery_beat.PeriodicTask so the schedule registers with the beat.

Trigger reference

KeyFires when
note.createdA notes.Notes row is inserted. Payload: full note dict + category, subcategory, media_source for filtering.
note.status_changedNotes.status changes. Payload includes both from_status and to_status.
note.status_unchangedA note has been in the same status for N days. Daily sweep at 13:00 UTC (automations.sweep_status_unchanged).
note.assignment_changedAssignees added or removed. Payload includes added_users / removed_users.
note.due_date_setA note gains a due_date value.
note.due_date_arrivedA note’s due_date is today. Hourly sweep at minute 5 (automations.sweep_due_date_arrivals).
note.custom_field_setA custom field on a note is set or changed. Payload includes field_name, value.
KeyFires when
kb.document_addedA Document enters STATUS_READY (after chunking + embedding). Useful for notifying the team a new doc is searchable.
KeyFires when
creative.hook_exhaustedA (client × archetype) transitions from healthy to exhausted. Payload: archetype, spend_share, roas_slope, net_new_ratio, exhaustion_score, recommended_alternatives. See Hook Exhaustion.
KeyFires when
agent.run_succeededA LangGraph swarm run completes without error. Payload: agent_id, run_id, final_message.
agent.run_failedA swarm run errors. Payload includes the exception class + first 2KB of the trace. Use this for Slack alerts on production failures.
KeyPattern
schedule.tickFree-form cron (crontab(...) expression).
schedule.weeklyDay-of-week + hour:minute.
schedule.monthlyDay-of-month + hour:minute. Handles short-month edge cases.
Each registers a PeriodicTask row in django-celery-beat. Cancelling the rule deletes the periodic task too.
KeyPattern
webhook.incomingThe rule is given a unique inbound URL. POST any JSON body — it becomes the trigger payload. Useful for integrations not directly supported (Zapier, n8n, etc). Auth via a per-rule shared secret in the URL.

Action reference

KeyPurpose
send_slackPost a Block Kit or text message to a Slack channel via the connected workspace. Honors per-tenant rate limits.
send_emailSend a transactional email via the configured SMTP. Subject + body support {{ payload.* }} templates.
inline_comment_mentionPost a comment on a note. Supports @user mentions that fan out as in-app notifications.
KeyPurpose
change_statusSet a note’s status. Idempotent (no-op if already in target state).
assign_usersAdd or remove assignees.
set_due_dateSet or clear due_date.
set_custom_fieldSet a custom field on the note.
update_noteBulk update note fields (title, body, priority, …).
apply_templateApply a saved note template — clones fields and creates the template’s subtasks under the trigger note.
create_subtasksCreate one or more subtasks under a parent note.
create_or_move_taskCreate a new note or move an existing one between folders.
KeyPurpose
add_to_kbWrite a Document into a knowledge base. Body, title, mime type, and target KB are configurable. Triggers ingest_document automatically.
KeyPurpose
webhook_callOutbound HTTPS call to a configured URL with the trigger payload (or a custom body). Supports retries with exponential backoff.
KeyPurpose
run_agentInvoke a LangGraph swarm with the trigger payload as initial context. Configure: which agent (Houston / Space Bob / Social Sally / a custom agent), system prompt overrides, optional Slack post-back of the agent’s final message.
run_agent_chainInvoke another automation rule by id. Enables multi-stage workflows.

Run history

Every dispatch produces an AutomationRun row:
FieldMeaning
run_idUUID.
automationFK to the rule.
trigger_keyThe trigger that fired.
payloadFull trigger payload (for replay).
statusqueued / running / succeeded / failed / cancelled.
started_at, finished_atWall clock.
step_logsList of per-action log lines (success, error, output snippet).
chained_fromSet when this run was kicked off by run_agent_chain from another rule.
The Run History view (/automations/history) is server-side paginated, filterable by trigger / action / status / date range / client. Click a row → see the original payload and per-step logs.

Replay

POST /api/automations/runs/<run_id>/replay/ re-dispatches the original payload through the rule’s current configuration. Useful when a rule’s action set changes and you want to retroactively apply the new chain. The replayed run is tagged with replayed_from=<original_run_id> for audit.

Cancel

POST /api/automations/runs/<run_id>/cancel/ revokes any queued actions in the chain. In-flight actions complete; downstream actions are skipped. The run finishes with status='cancelled'.

Slack alerts on failure

Pattern: chain agent.run_failedsend_slack:
Trigger:   agent.run_failed
Filters:   none
Actions:
  send_slack:
    channel: #alerts-mc-prod
    text: |
      :rotating_light: Agent run failed
      Agent: {{ payload.agent_id }}
      Run id: {{ payload.run_id }}
      Error: `{{ payload.error_class }}: {{ payload.error_message[:200] }}`
      [ View run ]({{ payload.run_url }})
Same pattern works for automation.run_failed (which is implicit — failed automation runs auto-emit agent.run_failed if a run_agent action was the failing step).

Idempotency + dedup

Each rule has an optional dedup_key template. The engine hashes the rendered key + the rule id and skips dispatch if a run with that hash exists in the last 30 minutes. Useful for noisy triggers — for example, note.status_changed can fire many times during bulk imports.

Rate limiting

Per-rule rate limits (e.g., “max 5 dispatches per minute per client_id”) are stored on the rule and enforced in automations.engine.dispatch before queuing. Slack actions additionally honor a per-workspace token-bucket so we don’t get rate-limited by Slack itself.

Where the code lives

ConcernPath
Engine + dispatchautomations/engine.py
Trigger / action registriesautomations/registry.py
Triggers + signal emittersautomations/triggers/__init__.py, automations/signals.py
Sweeps (cron-driven triggers)automations/sweeps.py
Actionsautomations/actions/*.py
Run modelautomations/models.py:AutomationRun
API viewsautomations/api/views.py
WebSocket consumerautomations/consumers.py
Live operationsautomations/live.py
Frontend automation builderl5ui/src/app/components/automations/
Run history viewl5ui/src/app/components/automations/operations/