# Automations

Lifecycle sequences on a visual canvas: 17 triggers, waits, branches, and 11 actions. The same automation is a YAML file you can review in a pull request.

An automation is a flow. Something happens to a contact, then a sequence of waits, branches, and actions runs for that one person, over minutes or over months. You build it on a canvas or you write it as YAML, and those are two views of one document rather than two features.

## Seventeen triggers, in three shapes

Fourteen arrive as events from elsewhere in the product: contact.subscribed, contact.added, contact.removed, contact.unsubscribed, tag.added, tag.removed, attribute.changed, email.delivered, email.opened, email.clicked, email.bounced, email.complained, email.received, and broadcast.sent. Two run on a clock: date.attribute for a renewal or a birthday, and schedule.recurring. One is yours to call: api.call, which starts a run from your own code and requires an idempotency key so a retried deploy hook does not enrol somebody twice.

## Waits that are the three waits people actually mean

A duration, written the way you would say it: wait: 3 days. A time of day in UTC, optionally on a weekday: wait: { until: monday 09:00 }. Or an event with a timeout: wait: { for: email.opened, timeout: 3 days }, which releases early the moment that event fires for that contact and otherwise carries on when the timeout expires. The sweep runs every five minutes, so a step happens within five minutes of its time rather than to the second.

## Eleven actions, and the list is the whole blast radius

send_template, send_audience, add_tag, remove_tag, add_to_audience, remove_from_audience, set_attribute, unsubscribe, suppress, call_webhook, notify_team. There is no "run this code", no "call this function", and no way to delete anything. An automation is a mailing sequence, so the worst a flow somebody pasted in from an assistant can do is bounded by that list. Unknown actions and unknown arguments are refused when you save, not discovered at 3am when a run reaches step nine.

## A run is a row, checkpointed after every effect

Each contact’s journey is one run with its own cursor: which branches are paused and until when, which steps have executed, and what each step returned, which later steps can read back as {{ steps.<id>.something }}. Starting a step is an insert that can fail, which is the mechanism that makes a double send impossible rather than unlikely. A run in flight is pinned to the version of the automation it started on, so editing a sequence does not change what is already halfway through it.

## Bounded on purpose

Up to 80 steps in a flow, up to 200 executed steps in one run counting resumes, at most 10 sends per run, and at most 500 runs an hour for one automation. The graph has to be acyclic and a cycle is refused when you save. A contact enters once by default; re_enter lets them come back after the previous run finishes, and always allows a concurrent one.

## The API

| Endpoint | What it does |
| --- | --- |
| `POST /v1/automations` | Create one from a YAML document, or from JSON carrying a yaml field. |
| `GET /v1/automations/:id.yaml` | The document as bytes, with the current version in a response header. |
| `PUT /v1/automations/:id.yaml` | Replace it with the document in the request body. |
| `PATCH /v1/automations/:id` | Enable or disable it, or replace the YAML. |
| `GET /v1/automations/:id/versions` | The last 50 versions, each with its full YAML, and a POST to restore one. |
| `GET /v1/automations/:id/runs` | Runs with status, steps executed, emails sent, and the next resume time. |
| `POST /v1/automations/:id/trigger` | Start a run from your own code. Requires an idempotency key. |

## A trial sequence, as the file

This is the whole automation. Steps flow into the one written under them unless they say otherwise.

```
name: Trial nudges
description: Three touches over the first week of a trial.
trigger: contact.subscribed
when:
  audience: Trials
reentry: once
steps:
  - id: welcome
    do: send_template
    with:
      template: trial-welcome
      variables:
        first_name: "{{ attributes.first_name }}"

  - id: wait_for_open
    wait:
      for: email.opened
      timeout: 3 days

  # Somebody already on a paid plan should not be nudged about the trial.
  - id: still_trialing
    if:
      all:
        - attributes.plan = trial
        - tags not_contains churned
    yes: nudge
    no: tag_converted

  - id: tag_converted
    do: add_tag
    with:
      tag: converted
    next: []

  - id: nudge
    do: send_template
    with:
      template: trial-day-3

  - id: wait_3d
    wait: 3 days

  - id: last_call
    do: send_template
    with:
      template: trial-day-7
```

## What this does not do

### There are no goals or exit conditions

A run ends by reaching a step with nothing after it, by an event wait timing out, or by erroring. There is no "stop this sequence when they convert" as a property of the automation, so the way to express it is a branch that ends the run, as the sample above does.

### Branches are two-way and the graph is acyclic

An if step has a yes and a no. There is no switch with five arms and no way to loop back to an earlier step. Cycles are refused when you save rather than allowed and capped at runtime.

### Waits are accurate to five minutes

The scheduler sweeps every five minutes. A wait until 09:00 means the run resumes in the five minutes after 09:00 UTC, not at 09:00:00.

### Times are UTC and the hour is the granularity

wait: { until: "09:30" } keeps the hour and drops the minutes, and there is no per-recipient timezone. A sequence that has to land at 9am local for everybody is not something this does today.

### A new automation is created disabled

Whatever the document says about enabled, creating one stores it switched off, so pushing a file cannot start mailing people before somebody has looked at it. Enabling is a separate, deliberate PATCH.

## Questions

### Can I write an automation as a file and review it in a pull request?

Yes, and that is the point of the format. The canvas and the YAML are two views of one document, the API serves and accepts the document at /v1/automations/:id.yaml, and a document you push back is stored as the exact bytes you sent rather than re-serialised. There is a page about the workflow at emails.sh/features/automations-as-code.

### What is an automation run, for billing?

One run is one contact going through one sequence, however many steps it has and however many days it takes. A paid plan includes 10,000 runs a month and additional runs are metered.

### Can an automation send the same person the same email twice?

No. Beginning a step is an insert that fails if that step has already begun for that run, so a retry resumes rather than repeats. A single run is also capped at 10 sends regardless of how the graph is shaped.

### Can an automation call my own code?

It can POST a signed automation.step event to a URL you own, with data you supply. It cannot execute code, run a query, or call an arbitrary internal function. The action list is closed and adding to it is a change to the product.

### Does an AI write the emails?

No. Nothing here generates copy. An automation sends templates you wrote, to contacts you have consent for, in an order you specified.

## Related

- [Automations as code](https://emails.sh/features/automations-as-code.md)
- [Broadcasts](https://emails.sh/features/broadcasts.md)
- [Audiences and segments](https://emails.sh/features/audiences-and-segments.md)
- [Templates](https://emails.sh/features/templates.md)

Docs: https://emails.sh/docs.md