> ## Documentation Index
> Fetch the complete documentation index at: https://docs.member.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Automations

> Build event-driven flows: when something happens in a hub, do something in response.

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.

```text theme={null}
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**.

```text theme={null}
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`:

```json theme={null}
{
  "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_id": "<vip tag id>" } },
          { "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.

<Note>
  `add_tag` / `remove_tag` node **configs** take a raw **`tag_id`** (a tag's UUID — copy it from `mio tags list`), not a `tag_slug`: the config schema is `{ "tag_id": "<uuid>" }` and rejects any other key (`{"tag_slug": …}` → *"Extra inputs are not permitted"*). Only branch **`has_tag` conditions** (and segment conditions) accept a `tag_slug`, which is resolved to an id at publish time.
</Note>

## Enrolling contacts

Most enrollments happen automatically when a trigger event fires. You can also enroll manually, or fire your own custom event from an integration.

```text theme={null}
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:

```json theme={null}
{
  "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.

<Note>
  `/test` is the safe way to confirm routing before you turn an automation on — branches evaluate for real, but every side effect is stubbed.
</Note>

## 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.

```text theme={null}
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.
