# Automations

Source: https://joinsimplesms.com/docs/automations
Index: https://joinsimplesms.com/llms.txt

Your app says what happened. SimpleSMS decides what to text and when.

```js
await sms.events.track({ user_id: "123", phone: "+14155550132", event: "trial_started" });
```

A **flow** starts on one event and runs a list of steps for that person:

```
trial_started
  -> Wait 1 hour
  -> Send SMS
  -> Wait until subscription_created
       arrives: end
       not in 3 days: continue
  -> Send SMS (reminder)
```

Build flows in [Console → Automations](/console/automations) or with the API
below. Three templates ship in the console: a welcome series, this trial
reminder, and an abandoned-onboarding nudge.

## Track an event

```bash
curl -X POST https://api.joinsimplesms.com/v1/track \
  -H "Authorization: Bearer $SIMPLESMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event":"trial_started","user_id":"123","phone":"+14155550132","properties":{"plan":"pro"}}'
```

| Field | |
| --- | --- |
| `event` | Required. Letters, digits, and `_ . : -`, up to 64 characters. |
| `phone` | The person's number in E.164. |
| `user_id` | Your own id for the person, up to 128 characters. |
| `properties` | Up to 20 values (text, number, true/false). Usable as merge fields and in conditions. |

Pass `phone`, `user_id`, or both. **A call with both links the two**, so
later events need only `user_id`. An event for a `user_id` that has never
been linked is stored, starts nothing, and answers with `"phone": null`.

The response lists what the event did:

```json
{
  "id": "tev_...",
  "object": "tracked_event",
  "event": "trial_started",
  "user_id": "123",
  "phone": "+14155550132",
  "test": false,
  "runs_started": ["run_..."],
  "runs_notified": 0
}
```

`Idempotency-Key` works as on `POST /v1/messages`. The limit is 100 calls
per 10 seconds per key. `GET /v1/events` is a different thing: the log of
[webhook events](/docs/webhooks) SimpleSMS sends you.

## Steps

| Step | Fields |
| --- | --- |
| `send_sms` | `body`, optional `from` (defaults to the flow's number) |
| `wait` | `seconds` (1 minute to 30 days) |
| `wait_for_event` | `event`, `timeout_seconds`, `on_received`, `on_timeout` |
| `condition` | `check`, `on_met`, `on_not_met` |
| `end` | none |

A branch (`on_received`, `on_timeout`, `on_met`, `on_not_met`) is
`"continue"`, `"end"`, or `"goto:<step id>"`. A `goto` may only point at a
**later** step, so a flow can never loop and a run sends at most one message
per send step.

A `check` is one of:

- `{ "kind": "event_received", "event": "onboarding_completed" }`: true if
  the person sent that event since the run started.
- `{ "kind": "property", "property": "plan", "op": "eq", "value": "pro" }`: a
  property of the event that started the run.
- `{ "kind": "contact_field", "field": "company", "op": "exists" }`: the
  [contact](/docs/contacts) with that number (`name`, `tag`, or a custom field).

Operators: `eq`, `neq`, `contains`, `gt`, `lt`, `exists`, `not_exists`.

`wait_for_event` counts the event if it arrived **any time since the run
started**, not only after the step began: someone who subscribed during an
earlier wait has subscribed.

Message bodies take [merge fields](/docs/broadcasts): every event property
by name (`{{plan}}`), plus `{{first_name}}`, `{{name}}`, `{{phone}}` and
`{{field:company}}` from the contact. A field with no value sends as
nothing; a message that merges to nothing is skipped.

## Create and activate

```bash
curl -X POST https://api.joinsimplesms.com/v1/automations \
  -H "Authorization: Bearer $SIMPLESMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Trial reminder",
    "from": "+15005550100",
    "trigger": { "event": "trial_started" },
    "steps": [
      { "type": "wait", "seconds": 3600 },
      { "type": "send_sms", "body": "Your trial is live, {{first_name}}. Reply STOP to opt out." },
      { "type": "wait_for_event", "event": "subscription_created",
        "timeout_seconds": 259200, "on_received": "end", "on_timeout": "continue" },
      { "type": "send_sms", "body": "Your trial ends soon. Pick a plan to keep your account." }
    ]
  }'
```

A flow is created as a `draft`. The response carries `issues`: everything
that stops it from running, each naming its step. `POST
/v1/automations/{id}/activate` turns it on (and answers 400 with the same
`issues` if it is not ready); `POST /v1/automations/{id}/pause` stops new
runs and holds the ones in progress where they are. Activating again resumes
them within 5 minutes.

A trigger can filter on properties:
`"trigger": { "event": "trial_started", "filters": [{ "property": "plan", "op": "eq", "value": "pro" }] }`.

| Endpoint | |
| --- | --- |
| `GET /v1/automations` | All flows, with run counts |
| `POST /v1/automations` | Create a draft |
| `GET` / `PATCH` / `DELETE /v1/automations/{id}` | Read, edit, delete |
| `POST /v1/automations/{id}/activate` · `/pause` | Turn on, hold |
| `GET /v1/automations/{id}/runs` | Runs, newest first |
| `GET` / `DELETE /v1/automations/{id}/runs/{run_id}` | One run with its timeline; cancel it |

All of it needs the `automations` scope on a restricted key.

## Sandbox and live

The flow's `from` number decides. A flow sending from a sandbox number
listens only to events tracked with a **test key**, and behaves like any
sandbox send: [magic numbers](/docs/sandbox) work, nothing is billed, and only
your own verified phone gets a real text. A flow sending from a live number
listens only to **live-key** events. So the same event name can drive a
sandbox copy and a live copy of a flow without either seeing the other's
traffic.

## What a run guarantees

- **One run per person per flow.** A repeat of the trigger while a run is in
  progress is ignored (counted as `triggers_skipped`). Once the run ends, the
  next trigger starts a new one.
- **No duplicate texts.** Each send step is attempted at most once. If our
  worker fails mid-send, the step is recorded as `send_unconfirmed` and not
  retried.
- **Edits never change a run in flight.** Saving an active flow publishes a
  new version; runs finish on the version they started with.
- **Opt-outs win.** Every message goes through the same checks as
  `POST /v1/messages`: the [opt-out list](/docs/opt-out), content screening,
  quotas and your [spend limit](/docs/spend-limits). A run whose recipient
  has opted out ends (`opted_out`) the next time it wakes, before anything
  is sent. Set `topic_id` on a flow to also skip people unsubscribed from
  that topic.
- **A failed send does not stop the run.** It is recorded on the run's
  timeline with the error code and the run continues. Transient carrier
  failures are retried by the normal [message retry](/docs/errors) path.
- **Waits are checked every minute.** A step due at 14:00:20 runs by 14:01.
  A first step that sends goes out with the track call itself.

## Runs

```json
{
  "id": "run_...",
  "object": "automation_run",
  "automation_id": "auto_...",
  "status": "waiting",
  "to": "+14155550132",
  "step": 2,
  "waiting_for_event": "subscription_created",
  "waiting_until": "2026-10-07T15:00:00.000Z",
  "end_reason": null,
  "messages_sent": 1,
  "timeline": [
    { "at": "...", "type": "started", "detail": "trial_started" },
    { "at": "...", "type": "sent", "step_id": "s2", "message_id": "msg_..." },
    { "at": "...", "type": "waiting_for_event", "step_id": "s3", "detail": "subscription_created" }
  ]
}
```

`status` is `running`, `waiting`, `completed`, or `failed`.
`end_reason` says why: `finished`, `end_step`, `branch_end` (completed);
`opted_out`, `canceled`, `flow_deleted`, `expired` (a run older than 90
days), `error` (failed).

Three [webhook events](/docs/webhooks) follow a run:
`automation.run.started`, `automation.run.completed` and
`automation.run.failed`, each with `automation_id`, `run_id`, `to` and,
at the end, `reason`, `messages_sent` and `messages_failed`.

## Limits and retention

- 20 steps per flow, 50 flows per account, 20 active at once, 5 trigger filters.
- Waits and timeouts: 1 minute to 30 days. A run ends after 90 days whatever
  it is waiting for.
- Tracked events are kept for 30 days. A run is kept for 30 days after it
  ends. The `user_id` to phone links are kept until you close your account.
