# Welcome every new reader

> The first email somebody gets from you decides whether they open the second. Commune can send it the moment they subscribe, follow it up a few days later, and stop the moment they tell you to. You write the two emails once.

How: with your agent (One prompt), or with the API (About 30 minutes, once).

What you will have:

- A sequence that greets every new reader within the minute, and follows up three days later.
- A copy of each email in your own inbox, read and approved before a subscriber ever sees one.
- One row per reader saying where they are in the sequence and what each email did for them.

**Warning:** Turning a sequence on installs a standing instruction. From that moment Commune emails whoever the trigger names, as often as it fires, with no further request from anybody, until a person turns it off. Everything before that step is a draft and sends nothing.

There are 2 ways to do this. Pick one: with your agent, or with the API.

## With your agent

Your agent can write the whole sequence, send you a test copy and leave it switched off until you have read it. **This route writes.** It creates a draft automation on your newsletter and mails a test copy to you, so give it a newsletter you are happy for it to add a draft to, and read the test copy before you say the word that turns it on.

Connect once: add the MCP server at `https://api.usecommune.com/mcp` to your agent and approve the consent screen ([how](https://usecommune.dev/guides/mcp)). Then ask:

```text
Read my Commune newsletter's automation triggers, then write me a welcome
sequence as a draft: an email as soon as somebody subscribes, a three day
wait, then a second email with a few things worth reading. Keep the
unsubscribe link and the mailing address in both.

Do not turn it on. Send me a test copy of both emails first and tell me
what you wrote.
```

The tools it uses:

- `which_newsletters_can_i_reach`: Lists the newsletters this connection was granted, so the draft lands on the one you meant.
- `commune_read`: Reads the trigger catalog (which events an automation can be built on, and what a condition may ask about under each), and afterwards the automation and its runs.
- `commune_write`: Writes the graph as a draft, sends the test copies, and later turns the sequence on when you ask. A draft sends nothing to your list; activation is the one call that does, and your client will ask you first because the operation says it cannot be undone.

What comes back, for example:

**Written as a draft, not turned on.** "Welcome sequence", on `subscriber.created`, three steps:

| Step | What it does | When |
|---|---|---|
| `welcome` | "Welcome, {{ subscriber.first_name }}" | Immediately |
| `settle` | Wait | 3 days |
| `best-of` | "Three things worth reading" | After the wait |

- **Test copies sent** to you at the address on your account. Both rendered with the placeholders filled in from example values.
- **Both emails carry the unsubscribe link and your mailing address**, which Commune refuses to send without.
- **Nobody on your list has received anything.** Say the word and I will turn it on; from then on it runs on its own.

Worth asking next:

- "Turn it on, and tell me how many readers enter it this week."
- "Add a branch: if they already hold the Founding member tag, skip the second email."
- "Who is part way through the sequence right now, and which step are they on?"

Tips:

- Ask for the test copy in the same prompt. A sequence is written once and then sent unattended for months, so the look-before-it-ships step is the one that actually matters, and it is easy to leave for later and never do.
- A sequence running on a list you imported is the surprise worth knowing about: an import publishes no new-subscriber event, so a migrated list is never welcomed. Adding addresses one at a time does publish it, and does welcome them.
- Say "as a draft" out loud. An agent that is told to build a welcome sequence and nothing else may reasonably finish the job.

## With the API

Five requests, in an order that is the safety property rather than a preference: find the newsletter, read what the trigger carries, write the graph whole, read the email yourself, and only then turn it on. A sixth tells you who is in it.

Before you start:

- An API key from https://usecommune.com/settings/api-keys with: sending: write. Export it as `COMMUNE_API_KEY`.
- Two email addresses you can read, for the test copies. They do not have to be subscribers.
- A newsletter Commune sends the email for, with a verified sending address. A sequence that only moves tags works on any newsletter; one that sends is refused when you turn it on otherwise.
- Some steps need more than this. Each one lists it where it starts.

Every request sends `Authorization: Bearer $COMMUNE_API_KEY` and `Commune-Version: 2026-08-26`. The requests below are also a collection you can import into Postman, Insomnia, Bruno, Yaak or Hoppscotch: https://usecommune.dev/use-cases/welcome-new-subscribers/collection.json

### 1. Find your newsletter

A key reaches one newsletter or several, whichever its owner ticked when creating it. List them and pick yours by `handle` (or `id`, either works in a path): the automation is written under it.

`GET /newsletters` ([reference](https://api-reference.usecommune.dev/operation/operation-listnewsletters))

```sh
curl "https://api.usecommune.com/newsletters" \
  -H "Authorization: Bearer $COMMUNE_API_KEY" \
  -H "Commune-Version: 2026-08-26"
```

Response `200`:

```json
{
  "object": "list",
  "data": [
    {
      "object": "newsletter",
      "id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411",
      "handle": "example-letter",
      "name": "The Example Letter",
      "description": "A weekly letter about how newsletters and communities fit together.",
      "esp": "commune",
      "image_url": "https://cdn.example.com/newsletters/example-letter/avatar.png",
      "website_url": "https://example.com",
      "social_links": {},
      "language": "en",
      "chat_create_permission": "subscribers",
      "allow_non_subscriber_chat": false,
      "owner": {
        "object": "user",
        "id": "usr_2Nf8Kq1pWc"
      },
      "created_at": "2025-03-04T10:00:00Z",
      "updated_at": "2026-08-26T09:32:11Z"
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  }
}
```

### 2. Read what the trigger carries

Every event Commune publishes is a trigger. This is the catalog, and it answers two questions you need before writing anything: whether a topic's events are about a subscriber (`carries_subscriber`), and what a condition may ask about under it (`context`).

`subscriber.created` is the one a welcome sequence waits for, and `carries_subscriber` is `true`, which is what makes a `send_email` step legal: a trigger whose events are not about a person cannot send an email or move a tag, and saving one that tries is refused rather than left to fail once per event forever.

Any credential that reaches a newsletter can read this. It describes Commune's event vocabulary rather than anybody's data.

`GET /automation-triggers` ([reference](https://api-reference.usecommune.dev/operation/operation-listautomationtriggers))

```sh
curl "https://api.usecommune.com/automation-triggers?limit=100" \
  -H "Authorization: Bearer $COMMUNE_API_KEY" \
  -H "Commune-Version: 2026-08-26"
```

Response `200`:

```json
{
  "object": "list",
  "data": [
    {
      "object": "automation_trigger",
      "topic": "article.published",
      "carries_subscriber": false,
      "context": {
        "trigger": "article.published",
        "event": [
          "article_id",
          "audience_scoped",
          "published_at",
          "slug",
          "source",
          "title",
          "url"
        ],
        "subscriber": [],
        "steps": []
      }
    },
    {
      "object": "automation_trigger",
      "topic": "subscriber.created",
      "carries_subscriber": true,
      "context": {
        "trigger": "subscriber.created",
        "event": [
          "acquisition_source",
          "created_at",
          "email",
          "resubscribed",
          "status",
          "subscriber_id",
          "user_id"
        ],
        "subscriber": [
          "acquired_via_commune",
          "acquisition_source",
          "created_at",
          "email",
          "id",
          "status",
          "subscribed_via",
          "tags"
        ],
        "steps": []
      }
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  }
}
```

### 3. Write the whole sequence in one request

`steps` is every step the sequence will have, and every edge between them is written down: `welcome` names `settle`, `settle` names `best-of`, and `best-of` names nothing, so the run ends there. A `next` left out is not "the next step", it is no step, which ends the run (it defaulted to the following entry until `2026-10-14`). There is no operation that adds one step later: a step is only meaningful against the edges pointing at it.

The emails arrive inline as `body_markdown`, parsed by the same strict parser article bodies use. **Keep the unsubscribe link and `{{ address }}` in every body.** Commune seeds both into a draft as ordinary content, which means they can be written away, and a body missing either is refused with `missing_footer` rather than sent. Every image in a body is probed too, because an inbox fetches images fresh when the reader opens the email.

`reentry: false` means once ever, however many times the trigger fires for that person. A resubscribe never re-enters whatever this says: an address coming back to a list it was on before does not get a first welcome email for the second time.

It comes back `draft`, and a draft cannot email anybody.

`POST /newsletters/{newsletter}/automations` ([reference](https://api-reference.usecommune.dev/operation/operation-createautomation))

```sh
curl -X POST "https://api.usecommune.com/newsletters/example-letter/automations" \
  -H "Authorization: Bearer $COMMUNE_API_KEY" \
  -H "Commune-Version: 2026-08-26" \
  -H "Idempotency-Key: welcome-sequence-2026-10-01" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Welcome sequence",
  "trigger": "subscriber.created",
  "reentry": false,
  "steps": [
    {
      "id": "welcome",
      "type": "send_email",
      "next": "settle",
      "email": {
        "subject": "Welcome, {{ subscriber.first_name }}",
        "preview_text": "Here is where to start.",
        "body_markdown": "# Glad you are here\n\nYou just subscribed to The Example Letter, which means a short piece about\nnewsletters and communities lands in your inbox every Thursday.\n\nReply to this one and tell me what you are working on. I read all of them.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    },
    {
      "id": "settle",
      "type": "wait",
      "duration": "P3D",
      "next": "best-of"
    },
    {
      "id": "best-of",
      "type": "send_email",
      "email": {
        "subject": "Three things worth reading",
        "body_markdown": "# Three things worth reading\n\nThese explain what the letter is about better than a welcome email can.\n\n- Why a newsletter is not a broadcast\n- The twenty readers who decide whether you keep going\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    }
  ]
}'
```

Response `201`:

```json
{
  "object": "automation",
  "id": "9c1e4b7a-2d35-4f61-8a90-1b2c3d4e5f60",
  "newsletter": {
    "object": "newsletter",
    "id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411"
  },
  "name": "Welcome sequence",
  "status": "draft",
  "trigger": "subscriber.created",
  "trigger_filter": null,
  "reentry": false,
  "steps": [
    {
      "id": "welcome",
      "type": "send_email",
      "next": "settle",
      "then": null,
      "else": null,
      "duration": null,
      "tag": null,
      "schema": null,
      "email": {
        "subject": "Welcome, {{ subscriber.first_name }}",
        "preview_text": "Here is where to start.",
        "body_markdown": "# Glad you are here\n\nYou just subscribed to The Example Letter, which means a short piece about\nnewsletters and communities lands in your inbox every Thursday.\n\nReply to this one and tell me what you are working on. I read all of them.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    },
    {
      "id": "settle",
      "type": "wait",
      "next": "best-of",
      "then": null,
      "else": null,
      "duration": "P3D",
      "tag": null,
      "schema": null,
      "email": null
    },
    {
      "id": "best-of",
      "type": "send_email",
      "next": null,
      "then": null,
      "else": null,
      "duration": null,
      "tag": null,
      "schema": null,
      "email": {
        "subject": "Three things worth reading",
        "preview_text": null,
        "body_markdown": "# Three things worth reading\n\nThese explain what the letter is about better than a welcome email can.\n\n- Why a newsletter is not a broadcast\n- The twenty readers who decide whether you keep going\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    }
  ],
  "created_at": "2026-10-01T09:00:00Z",
  "updated_at": "2026-10-01T09:00:00Z",
  "activated_at": null
}
```

### 4. Read the email before a subscriber does

You will need:

- Two addresses you can read. Neither has to be a subscriber.

This is the step people skip, and it is the one that matters most here. An article is written, looked at and sent within an afternoon. An automation email is written once and then sent, unattended, for months, to people who arrive one at a time, so there is no moment at which anybody watches it go out. Without this the first person to see it rendered is a subscriber.

It renders through the same renderer and the same personalization pass the real send uses, so what lands is what a subscriber would get: the placeholders filled in from example values, and the unsubscribe link pointing at a page that explains itself rather than at a live token. It touches nothing else, so run it as many times as you need to.

Up to five addresses. Leave `to` out and the copy goes to the address on the account this credential belongs to, which is why `recipients` can come back empty with `sent_to_owner: true`.

Repeat it for `best-of`. The step id in the path is the one you chose when you wrote the graph.

`POST /automations/{automation}/steps/{step}/send-test` ([reference](https://api-reference.usecommune.dev/operation/operation-sendautomationsteptest))

```sh
curl -X POST "https://api.usecommune.com/automations/9c1e4b7a-2d35-4f61-8a90-1b2c3d4e5f60/steps/welcome/send-test" \
  -H "Authorization: Bearer $COMMUNE_API_KEY" \
  -H "Commune-Version: 2026-08-26" \
  -H "Idempotency-Key: welcome-test-2026-10-01" \
  -H "Content-Type: application/json" \
  -d '{
  "to": [
    "me@example.com",
    "editor@example.com"
  ]
}'
```

Response `200`:

```json
{
  "object": "automation_step_test",
  "automation": {
    "object": "automation",
    "id": "9c1e4b7a-2d35-4f61-8a90-1b2c3d4e5f60"
  },
  "step": "welcome",
  "subject": "Welcome, Alex",
  "recipients": [
    "me@example.com",
    "editor@example.com"
  ],
  "sent_to_owner": false,
  "sent": 2,
  "failed": 0
}
```

### 5. Turn it on

**This is the request that starts sending.** Everything above describes a sequence; this installs it. From the moment it returns, Commune mails whoever the trigger names, as often as it fires, with no further request from anybody, until a person turns it off.

Every gate runs again here, on the stored automation, and the repetition is deliberate: a sequence can be saved by one tool and turned on by another weeks later, and in between an image can rot, a tag a step applies can be deleted, and a sending domain can fall out of verification. Each is a `422` naming what is wrong: the graph, a condition, `missing_footer`, `broken_images`, or the newsletter having no verified sending address.

It is charged to a daily cap of its own, counted per newsletter, and reported as a `429` that is not one of the `RateLimit-Policy` budgets: its `Retry-After` is measured in hours, which is how to tell it from an ordinary rate limit. An activation that was refused costs nothing, so fixing a graph and trying again is free until it succeeds.

Turning on a sequence that is already `active` succeeds and changes nothing. To stop it later, pause it: a pause holds every waiting run exactly where it is, and there is no window in which a paused automation still sends.

**Nothing recalls mail that has left.** Read the test copies first. This request cannot be undone in the sense that matters: pausing stops the next email, and it does not unsend the ones already delivered.

`POST /automations/{automation}/activate` ([reference](https://api-reference.usecommune.dev/operation/operation-activateautomation))

```sh
curl -X POST "https://api.usecommune.com/automations/9c1e4b7a-2d35-4f61-8a90-1b2c3d4e5f60/activate" \
  -H "Authorization: Bearer $COMMUNE_API_KEY" \
  -H "Commune-Version: 2026-08-26" \
  -H "Idempotency-Key: welcome-activate-2026-10-01"
```

Response `200`:

```json
{
  "object": "automation",
  "id": "9c1e4b7a-2d35-4f61-8a90-1b2c3d4e5f60",
  "newsletter": {
    "object": "newsletter",
    "id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411"
  },
  "name": "Welcome sequence",
  "status": "active",
  "trigger": "subscriber.created",
  "trigger_filter": null,
  "reentry": false,
  "steps": [
    {
      "id": "welcome",
      "type": "send_email",
      "next": "settle",
      "then": null,
      "else": null,
      "duration": null,
      "tag": null,
      "schema": null,
      "email": {
        "subject": "Welcome, {{ subscriber.first_name }}",
        "preview_text": "Here is where to start.",
        "body_markdown": "# Glad you are here\n\nYou just subscribed to The Example Letter, which means a short piece about\nnewsletters and communities lands in your inbox every Thursday.\n\nReply to this one and tell me what you are working on. I read all of them.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    },
    {
      "id": "settle",
      "type": "wait",
      "next": "best-of",
      "then": null,
      "else": null,
      "duration": "P3D",
      "tag": null,
      "schema": null,
      "email": null
    },
    {
      "id": "best-of",
      "type": "send_email",
      "next": null,
      "then": null,
      "else": null,
      "duration": null,
      "tag": null,
      "schema": null,
      "email": {
        "subject": "Three things worth reading",
        "preview_text": null,
        "body_markdown": "# Three things worth reading\n\nThese explain what the letter is about better than a welcome email can.\n\n- Why a newsletter is not a broadcast\n- The twenty readers who decide whether you keep going\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    }
  ],
  "created_at": "2026-10-01T09:00:00Z",
  "updated_at": "2026-10-01T09:00:00Z",
  "activated_at": "2026-10-02T11:16:00Z"
}
```

### 6. See who is in it

One row per subscriber who entered the sequence, most recent first. `status` and `current_step` say where they are, `next_run_at` says when they move, and `steps` is what each email did for that one person: sent, delivered, opened, clicked, bounced, complained, each with its own time.

`?status=waiting` is the question you will actually ask: who is part way through right now. `?subscriber=` answers "did this sequence reach that person", and returns one row unless the automation allows re-entry.

**No row carries an email address.** A run is read under `sending` and an address belongs to `audience`, so `subscriber` is a reference that `?expand=subscriber` deliberately will not inflate. Resolve it with the subscriber endpoint if you need the address, under a key that holds `audience: read`.

`GET /automations/{automation}/runs` ([reference](https://api-reference.usecommune.dev/operation/operation-listautomationruns))

```sh
curl "https://api.usecommune.com/automations/9c1e4b7a-2d35-4f61-8a90-1b2c3d4e5f60/runs?status=waiting&limit=100" \
  -H "Authorization: Bearer $COMMUNE_API_KEY" \
  -H "Commune-Version: 2026-08-26"
```

Response `200`:

```json
{
  "object": "list",
  "data": [
    {
      "object": "automation_run",
      "id": "55667788-99aa-4bbc-8dde-ff0011223344",
      "automation": {
        "object": "automation",
        "id": "9c1e4b7a-2d35-4f61-8a90-1b2c3d4e5f60"
      },
      "subscriber": {
        "object": "subscriber",
        "id": "33445566-7788-4990-a1b2-c3d4e5f60718"
      },
      "status": "waiting",
      "current_step": "settle",
      "exit_reason": null,
      "attempts": 2,
      "steps": [
        {
          "step": "welcome",
          "status": "opened",
          "failure_reason": null,
          "sent_at": "2026-10-06T08:00:01Z",
          "delivered_at": "2026-10-06T08:00:09Z",
          "opened_at": "2026-10-06T09:14:22Z",
          "clicked_at": null,
          "bounced_at": null,
          "complained_at": null
        }
      ],
      "next_run_at": "2026-10-09T08:00:01Z",
      "created_at": "2026-10-06T08:00:00Z",
      "completed_at": null
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  }
}
```

### 7. Hear about it instead of polling

Three topics cover a run's whole life, so a consumer does not have to poll: `automation.run.started` when somebody enters, and then exactly one of `automation.run.completed` or `automation.run.exited`.

`automation.run.exited` is the interesting one. It carries both the ordinary early endings and the failures, under `data.status`: `exited` is somebody who unsubscribed, bounced or complained part way through, which Commune re-checks immediately before every send rather than trusting what it saw when the run began; `failed` is a run Commune could not advance and has given up on, which is the one that wants a person.

`actor` names the run rather than a credential, and that is the same attribution Commune puts on the events a run's own actions cause. `data.chain_depth` is how many automations' actions led here, out of a limit of five.

Destinations are registered in the delivery portal rather than through this API. The event delivery section of the reference has that.

Event `automation.run.completed`, as Commune posts it to your endpoint:

```json
{
  "id": "018f2a93-7070-7000-8000-000000000071",
  "type": "automation.run.completed",
  "api_version": "2026-08-26",
  "occurred_at": "2026-08-31T09:00:04Z",
  "newsletter_id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411",
  "actor": {
    "type": "system",
    "id": "2b7c4d19-6e03-4f88-9a21-5c6d7e8f9a0b",
    "label": "automation: Welcome sequence"
  },
  "idempotency_key": null,
  "data": {
    "run_id": "2b7c4d19-6e03-4f88-9a21-5c6d7e8f9a0b",
    "automation_id": "9c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f",
    "automation_name": "Welcome sequence",
    "trigger": "subscriber.created",
    "subscriber_id": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b",
    "event_id": "018f2a93-5050-7000-8000-000000000051",
    "chain_depth": 0,
    "started_at": "2026-08-28T09:00:01Z",
    "completed_at": "2026-08-31T09:00:04Z"
  }
}
```

### All together

Steps 1, 3 and 4 as one file, with no dependencies. It stops before activation on purpose: turning a sequence on is the one call here that commits you to sending, and it belongs to a person reading a test copy rather than to a script.

```js
const BASE = "https://api.usecommune.com";
const VERSION = "2026-08-26";
const KEY = process.env.COMMUNE_API_KEY;

async function api(path, { method = "GET", body, idempotencyKey } = {}) {
  const res = await fetch(BASE + path, {
    method,
    headers: {
      authorization: `Bearer ${KEY}`,
      "commune-version": VERSION,
      ...(body ? { "content-type": "application/json" } : {}),
      ...(idempotencyKey ? { "idempotency-key": idempotencyKey } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  if (res.status === 429) {
    await new Promise((r) => setTimeout(r, Number(res.headers.get("retry-after") ?? 5) * 1000));
    return api(path, { method, body, idempotencyKey });
  }
  const answer = await res.json();
  if (!res.ok) {
    const { code, message, request_id } = answer.error;
    throw new Error(`${res.status} ${code}: ${message} (request ${request_id})`);
  }
  return answer;
}

// 1. The newsletter to write the sequence under.
const { data: newsletters } = await api("/newsletters");
const wanted = process.env.COMMUNE_NEWSLETTER;
const newsletter = wanted
  ? newsletters.find((n) => n.handle === wanted || n.id === wanted)
  : newsletters.length === 1 ? newsletters[0] : null;
if (!newsletter) {
  throw new Error(
    wanted
      ? "This key does not reach " + wanted + "."
      : "This key reaches " + newsletters.length + " newsletters. Set COMMUNE_NEWSLETTER to one handle.",
  );
}

// The footer the law requires and Commune refuses to send without. Written
// into every body rather than appended by us, because it is ordinary content
// and you may want it to read differently.
const FOOTER = "[Unsubscribe]({{ unsubscribe_url }}) \u00b7 {{ address }}";

// 2. The graph, whole, with every edge named. A `next` left out ends the run
//    at that step, so a sequence wires each one to the one after it.
const draft = await api(`/newsletters/${newsletter.handle}/automations`, {
  method: "POST",
  idempotencyKey: "welcome-sequence-v1",
  body: {
    name: "Welcome sequence",
    trigger: "subscriber.created",
    reentry: false,
    steps: [
      {
        id: "welcome",
        type: "send_email",
        next: "settle",
        email: {
          subject: "Welcome, {{ subscriber.first_name }}",
          preview_text: "Here is where to start.",
          body_markdown: `# Glad you are here\n\nReply and tell me what you are working on.\n\n${FOOTER}\n`,
        },
      },
      { id: "settle", type: "wait", duration: "P3D", next: "best-of" },
      {
        id: "best-of",
        type: "send_email",
        email: {
          subject: "Three things worth reading",
          body_markdown: `# Three things worth reading\n\nHere is where to start.\n\n${FOOTER}\n`,
        },
      },
    ],
  },
});

// 3. A copy of every email, to you, before anybody else sees one.
for (const step of draft.steps.filter((s) => s.type === "send_email")) {
  const test = await api(`/automations/${draft.id}/steps/${step.id}/send-test`, {
    method: "POST",
    idempotencyKey: `welcome-test-${step.id}-v1`,
    body: { to: [process.env.REVIEW_ADDRESS] },
  });
  console.log(`${step.id}: sent ${test.sent}, failed ${test.failed}, subject "${test.subject}"`);
}

console.log(
  `Draft ${draft.id} is written and NOT sending. Read the test copies, then:\n` +
    `  POST ${BASE}/automations/${draft.id}/activate`,
);
```
