Automations turn hub events into actions. Instead of a single linear drip, you build a flowchart: a trigger starts a run, and each contact moves through nodes — send an email, wait, branch on a condition, add or remove a tag, hand off to a drip sequence, call a webhook, or exit.
A drip sequence becomes one node inside an automation. A one-step automation (one trigger, one action) is a “rule”. Same engine for both.
WHEN a member joins
→ send "Welcome" email
→ wait 2 days
→ has the "engaged" tag?
yes → add "vip" tag
no → send "Need help?" email
→ end
How it works
- Triggers subscribe an automation to an event type (e.g. a member joining, a purchase, a tag being applied, or a custom event you fire yourself).
- Each matching contact gets an enrollment — an independent run through the published graph. Enrollments are idempotent (one event enrolls a contact at most once) and honor a per-automation re-entry policy (
never, after_exit, interval).
- A published version is an immutable snapshot. Editing a draft and re-publishing creates a new version; in-flight enrollments keep running on the version they started on.
Node types
| Node | What it does |
|---|
send_email | Send a transactional email (template + merge context). |
wait | Pause the run for a duration (e.g. "2 days"). |
branch | Evaluate a segment condition tree → yes / no edge. |
add_tag / remove_tag | Tag or untag the contact (idempotent). |
enroll_in_sequence | Hand the contact off to a drip campaign (terminal). |
webhook | POST a signed payload to a configured endpoint (SSRF-guarded, async with retries). |
exit | End the run. |
Authoring lifecycle
Automations are hub-scoped and require a team owner. The lifecycle is draft → publish (snapshot + validate) → activate.
POST /api/v1/teams/{team_id}/hubs/{hub_id}/automations create draft
GET /api/v1/teams/{team_id}/hubs/{hub_id}/automations list
GET /api/v1/teams/{team_id}/hubs/{hub_id}/automations/{id} retrieve
PATCH /api/v1/teams/{team_id}/hubs/{hub_id}/automations/{id} update draft
POST /api/v1/teams/{team_id}/hubs/{hub_id}/automations/{id}/publish snapshot + validate graph
POST /api/v1/teams/{team_id}/hubs/{hub_id}/automations/{id}/activate draft/paused → active
POST /api/v1/teams/{team_id}/hubs/{hub_id}/automations/{id}/deactivate active → paused
GET /api/v1/teams/{team_id}/hubs/{hub_id}/automations/{id}/versions list versions
A create body carries the graph in attributes.definition:
{
"data": {
"type": "automations",
"attributes": {
"name": "Welcome flow",
"re_entry_mode": "never",
"definition": {
"triggers": [{ "event_type": "custom.member_joined" }],
"nodes": [
{ "id": "welcome", "type": "send_email", "config": { "template_ref": "welcome-email" } },
{ "id": "wait2d", "type": "wait", "config": { "duration": "2 days" } },
{ "id": "checktag", "type": "branch",
"config": { "condition_tree": { "groups": [ { "conditions": [
{ "type": "has_tag", "operator": "has", "value": { "tag_slug": "engaged" } }
] } ] } } },
{ "id": "tagvip", "type": "add_tag", "config": { "tag_slug": "vip" } },
{ "id": "nudge", "type": "send_email", "config": { "template_ref": "nudge-email" } },
{ "id": "done", "type": "exit", "config": {} }
],
"edges": [
{ "from": "welcome", "to": "wait2d" },
{ "from": "wait2d", "to": "checktag" },
{ "from": "checktag", "to": "tagvip", "branch_label": "yes" },
{ "from": "checktag", "to": "nudge", "branch_label": "no" },
{ "from": "tagvip", "to": "done" },
{ "from": "nudge", "to": "done" }
]
}
}
}
}
publish validates the graph (reachability, branch edges must be labeled, condition trees compile against your tags/segments) and returns a versioned snapshot. Branch tag_slug values are resolved to IDs at publish time, so the referenced tags must already exist.
Most enrollments happen automatically when a trigger event fires. You can also enroll manually, or fire your own custom event from an integration.
POST /api/v1/teams/{team_id}/hubs/{hub_id}/automations/{id}/enrollments manual enroll a contact
GET /api/v1/teams/{team_id}/hubs/{hub_id}/automations/{id}/enrollments observe runs (?filter[status]=stuck)
POST /api/v1/teams/{team_id}/hubs/{hub_id}/automations/events fire a custom event
POST /api/v1/teams/{team_id}/hubs/{hub_id}/automations/{id}/test dry-run a contact (no side effects)
Custom events require an idempotency_key (deduplicated per team) and identify the contact:
{
"event_type": "custom.member_joined",
"team_contact_id": "…",
"idempotency_key": "join-2026-06-09-abc",
"payload": {}
}
Returns 202 on a new event, 200 on a duplicate. Use /test to dry-run a contact through the published graph: it runs the real branch logic and reports the step trace without sending email, applying tags, or firing webhooks.
/test is the safe way to confirm routing before you turn an automation on — branches evaluate for real, but every side effect is stubbed.
Webhook endpoints
The webhook node POSTs a signed JSON payload to an endpoint you register. Targets are validated against an SSRF guard (private, loopback, link-local, and cloud-metadata addresses are rejected), the signing secret is encrypted at rest and never returned, and delivery is asynchronous with exponential-backoff retries.
POST /api/v1/teams/{team_id}/hubs/{hub_id}/webhook-endpoints create (returns no secret)
GET /api/v1/teams/{team_id}/hubs/{hub_id}/webhook-endpoints list
DELETE /api/v1/teams/{team_id}/hubs/{hub_id}/webhook-endpoints/{id} delete
Reliability
- Triggers are delivered from a durable event outbox, so a server hiccup never silently drops an enrollment.
- Each node executes at most once per enrollment (idempotency keys); failed steps retry, then move to a dead-letter queue rather than looping.
wait is real scheduling — a run resumes when its timer is due, even days later.