# Send a different email to different readers

> A sequence that sends everybody the same three emails treats a reader who has supported you for two years exactly like one who subscribed on Tuesday and has not opened anything since. Commune can ask, part way through, what it knows about the person and what they have done with the email you already sent, and take a different path for each.

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

What you will have:

- A sequence that asks, three days in, whether the reader holds a tag and whether they opened the first email.
- A different second email for each answer, and nothing at all for the path you decide should end.
- A branch you can change while people are part way through it, with the readers already in flight told where to go.

**Warning:** A condition about a property the run cannot see matches every run, so the branch silently always takes the same arm. Commune refuses such a condition when you save, naming the path, which is why the first request here reads the trigger catalog's context rather than guessing at field names.

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

## With your agent

Your agent can read the sequence you already have, add the branch and send you the new emails to look at. **This route writes**, replacing the graph of a live automation, and an edit applies to the readers already part way through it. Ask it to tell you the graph back before it sends anything.

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 welcome sequence, then add a branch three
days in:

- if the reader holds the Founding member tag, send them the members-only
  note instead of the usual second email, and end there;
- otherwise, if they opened the first email, send the usual second email;
- if they did not open it, send a short note asking them to check their
  spam folder.

Show me the graph before you save it, and send me a test copy of each new
email.
```

The tools it uses:

- `commune_read`: Reads the trigger catalog (which properties a condition may name under this trigger), the newsletter's tags to find the one to branch on, and the automation with its whole graph including the Markdown of every email.
- `commune_write`: Sends the graph back with the branch in it, and sends test copies of the new emails. Replacing the graph of a live automation applies to the runs already in flight.
- `who_is_on_my_list`: Answers how many readers hold the tag you are branching on, which is how you tell a branch worth building from one that will take the same arm every time.

What comes back, for example:

**Here is the graph before I save it.** Two conditions, three endings:

| Step | Asks | Then | Otherwise |
|---|---|---|---|
| `member` | Holds the Founding member tag? | `insider`, and the run ends | `read-it` |
| `read-it` | Opened `welcome`? | `best-of`, and the run ends | `nudge` |

- **214 readers hold Founding member**, so the first arm is worth having rather than theoretical.
- **The tag is matched by id, not by name**, so renaming it in Commune will not break the branch.
- **Both conditions carry `required`.** Without it a missing property matches everything and the branch would always take the same side.
- Say the word and I will save it. Test copies of `insider` and `nudge` are in your inbox.

Worth asking next:

- "How many readers have taken each arm this month?"
- "Take the nudge back out: the readers who never opened should get nothing more."
- "Which readers are sitting on the wait step right now?"

Tips:

- Ask what the condition can see before you describe the branch. A condition can read the event that started the run, the subscriber's record as it stands now, and whether an earlier email in this sequence was opened or clicked. It cannot read anything else, and a path it cannot see is refused when you save.
- A tag is read at the moment the condition runs, not when the run started. That is the whole value of branching three days in: somebody who upgraded on Tuesday takes the member arm on Thursday.
- An arm ends where its last step stops pointing anywhere, so you do not have to say "and then nothing". Commune used to fall through to the next step in order, which meant an arm that did not end ran into the following arm's email and the reader got both; that changed on 2026-10-14. There is no step type for ending a path either: a `stop` step existed for saying it out loud and was removed on 2026-10-15, because the arrow with nothing on the end of it already says it.

## With the API

Four requests and one read. Pull the graph you have, find the tag to branch on, send the graph back with the condition in it, look at the new emails, and read one run to see which arm it took. A fifth request takes an arm away again, which is the edit that has to say what happens to the readers standing on it.

Before you start:

- An API key from https://usecommune.com/settings/api-keys with: sending: write, audience: read. Export it as `COMMUNE_API_KEY`.
- A sequence that already exists. Welcome every new reader writes one; this page edits it.
- The `id` of that automation. It is in the list of the newsletter's automations, and in the answer to the request that created it.
- 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/branch-a-sequence/collection.json

### 1. Read the graph you already have

One automation with its whole graph: every step, every condition, and the Markdown of every email it sends. Read it first for a practical reason rather than a tidy one: `steps` on an update **replaces the whole graph**, so the request in step 3 has to carry the steps you are keeping as well as the ones you are adding.

The `body_markdown` that comes back is the rendition you sent, with the personalization placeholders as you wrote them, so reading an automation, changing one step and sending the graph back preserves everything else. Resolving the placeholders here would silently replace every greeting in the sequence with its fallback the next time anybody saved it.

Every field is present on every step, `null` where the type does not use it. That is the opposite of the request shape, which refuses a field a type does not use, and the asymmetry is deliberate: a reader wants one key set to narrow, and a writer wants to be told when it has said something meaningless. A body built by sending this back is accepted unchanged.

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

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

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\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\nA few pieces that explain what this letter is about better than I can in a\nwelcome email.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    }
  ],
  "created_at": "2026-10-01T09:00:00Z",
  "updated_at": "2026-10-02T11:16:00Z",
  "activated_at": "2026-10-02T11:16:00Z"
}
```

### 2. Find the tag you want to branch on

A condition names a tag by `id`, not by name, which is why this request exists: a name would be friendlier to write and would break the moment somebody renamed the tag in Commune.

`known_subscriber_count` is the number worth looking at before you build the branch. A tag nobody holds is a branch that will take the same arm every time, which is exactly the failure a condition is hardest to notice.

Retired tags are left out unless you ask for them. They still resolve the audience of articles sent before they were retired, but a step naming one is refused, because it would do nothing, every run, forever.

`GET /newsletters/{newsletter}/tags` ([reference](https://api-reference.usecommune.dev/operation/operation-listnewslettertags))

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

Response `200`:

```json
{
  "object": "list",
  "data": [
    {
      "object": "tag",
      "id": "aa11bb22-cc33-4d44-8e55-ff6677889900",
      "newsletter": {
        "object": "newsletter",
        "id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411"
      },
      "name": "Founding member",
      "known_subscriber_count": 214,
      "is_room": false,
      "retired": false,
      "retired_at": null,
      "created_at": "2025-06-11T08:45:00Z"
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  }
}
```

### 3. Send the graph back with the branch in it

Two conditions, and three endings.

`member` asks whether the reader holds the tag **now**, three days into the sequence rather than when they subscribed, which is the whole reason a condition is worth having. `read-it` asks whether the first email was opened, which Commune knows because every `send_email` step in a run reports `opened` and `clicked` once its delivery has been attributed.

**Both carry `required`, and that is not decoration.** JSON Schema treats an absent property as saying nothing, so `properties` alone asks "if this is present it has to look like this" and matches a run where it is missing. `required` is what asks for it to be there at all. A condition naming a path the trigger cannot produce is refused outright when you save, naming the path.

**Each arm ends by not pointing anywhere.** `insider`, `best-of` and `nudge` leave `next` out, and that ends the run: there are three endings and no step saying so. This graph used to need two such steps, because `next` defaulted to the following step in the array and without an `end-insider` the member arm fell straight into `read-it` and a member was asked the engagement question as well. That defaulting was removed on `2026-10-14`, and the `stop` type that was left over went on `2026-10-15`: an edge you leave out is the only way to end a path now, and it is the one that was always there.

The whole graph is checked before anything is stored: every edge points at a step that exists, every path ends, there are no cycles, every condition is in the keyword subset and names paths this trigger can produce, and every email still carries its unsubscribe link, its mailing address and images that load. Any of those is a `422` naming which.

**The edit applies to the runs already in flight.** A run sits on a step `id`, so a subscriber three days in picks up the new steps from wherever they are. That is the point of editing a live sequence rather than replacing it.

`PATCH /automations/{automation}` ([reference](https://api-reference.usecommune.dev/operation/operation-updateautomation))

```sh
curl -X PATCH "https://api.usecommune.com/automations/9c1e4b7a-2d35-4f61-8a90-1b2c3d4e5f60" \
  -H "Authorization: Bearer $COMMUNE_API_KEY" \
  -H "Commune-Version: 2026-08-26" \
  -H "Idempotency-Key: welcome-branch-2026-10-08" \
  -H "Content-Type: application/json" \
  -d '{
  "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 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": "member"
    },
    {
      "id": "member",
      "type": "condition",
      "schema": {
        "properties": {
          "subscriber": {
            "properties": {
              "tags": {
                "contains": {
                  "const": "aa11bb22-cc33-4d44-8e55-ff6677889900"
                }
              }
            },
            "required": [
              "tags"
            ]
          }
        },
        "required": [
          "subscriber"
        ]
      },
      "then": "insider",
      "else": "read-it"
    },
    {
      "id": "insider",
      "type": "send_email",
      "email": {
        "subject": "The part I only tell members",
        "body_markdown": "# The part I only tell members\n\nYou have been supporting this letter from early on, so here is the thing I\nleave out of the public archive: what I am working on next, and why.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    },
    {
      "id": "read-it",
      "type": "condition",
      "schema": {
        "properties": {
          "steps": {
            "properties": {
              "welcome": {
                "properties": {
                  "opened": {
                    "const": true
                  }
                },
                "required": [
                  "opened"
                ]
              }
            },
            "required": [
              "welcome"
            ]
          }
        },
        "required": [
          "steps"
        ]
      },
      "then": "best-of",
      "else": "nudge"
    },
    {
      "id": "best-of",
      "type": "send_email",
      "email": {
        "subject": "Three things worth reading",
        "body_markdown": "# Three things worth reading\n\nA few pieces that explain what this letter is about better than I can in a\nwelcome email.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    },
    {
      "id": "nudge",
      "type": "send_email",
      "email": {
        "subject": "Did this land in the wrong folder?",
        "body_markdown": "# Did this land in the wrong folder?\n\nThe first email I sent you is still unread, which usually means it went\nsomewhere other than your inbox. If you want it, dragging one of these into\nyour main folder is normally enough.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    }
  ]
}'
```

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\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": "member",
      "then": null,
      "else": null,
      "duration": "P3D",
      "tag": null,
      "schema": null,
      "email": null
    },
    {
      "id": "member",
      "type": "condition",
      "next": null,
      "then": "insider",
      "else": "read-it",
      "duration": null,
      "tag": null,
      "schema": {
        "properties": {
          "subscriber": {
            "properties": {
              "tags": {
                "contains": {
                  "const": "aa11bb22-cc33-4d44-8e55-ff6677889900"
                }
              }
            },
            "required": [
              "tags"
            ]
          }
        },
        "required": [
          "subscriber"
        ]
      },
      "email": null
    },
    {
      "id": "insider",
      "type": "send_email",
      "next": null,
      "then": null,
      "else": null,
      "duration": null,
      "tag": null,
      "schema": null,
      "email": {
        "subject": "The part I only tell members",
        "preview_text": null,
        "body_markdown": "# The part I only tell members\n\nYou have been supporting this letter from early on, so here is the thing I\nleave out of the public archive: what I am working on next, and why.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    },
    {
      "id": "read-it",
      "type": "condition",
      "next": null,
      "then": "best-of",
      "else": "nudge",
      "duration": null,
      "tag": null,
      "schema": {
        "properties": {
          "steps": {
            "properties": {
              "welcome": {
                "properties": {
                  "opened": {
                    "const": true
                  }
                },
                "required": [
                  "opened"
                ]
              }
            },
            "required": [
              "welcome"
            ]
          }
        },
        "required": [
          "steps"
        ]
      },
      "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\nA few pieces that explain what this letter is about better than I can in a\nwelcome email.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    },
    {
      "id": "nudge",
      "type": "send_email",
      "next": null,
      "then": null,
      "else": null,
      "duration": null,
      "tag": null,
      "schema": null,
      "email": {
        "subject": "Did this land in the wrong folder?",
        "preview_text": null,
        "body_markdown": "# Did this land in the wrong folder?\n\nThe first email I sent you is still unread, which usually means it went\nsomewhere other than your inbox. If you want it, dragging one of these into\nyour main folder is normally enough.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    }
  ],
  "created_at": "2026-10-01T09:00:00Z",
  "updated_at": "2026-10-08T10:04:00Z",
  "activated_at": "2026-10-02T11:16:00Z"
}
```

### 4. Look at the emails the new arms send

You will need:

- An address you can read. It does not have to be a subscriber.

Two new bodies went in with that edit, and nobody has read either of them rendered. A test copy is the only way anybody sees an automation email before a subscriber does, because people arrive one at a time and there is no moment at which somebody watches the sequence go out.

It renders through the same renderer and the same personalization pass the real send uses, touches no run and nobody on the list, and takes up to five addresses. Run it again for `nudge`.

`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/insider/send-test" \
  -H "Authorization: Bearer $COMMUNE_API_KEY" \
  -H "Commune-Version: 2026-08-26" \
  -H "Idempotency-Key: insider-test-2026-10-08" \
  -H "Content-Type: application/json" \
  -d '{
  "to": [
    "me@example.com"
  ]
}'
```

Response `200`:

```json
{
  "object": "automation_step_test",
  "automation": {
    "object": "automation",
    "id": "9c1e4b7a-2d35-4f61-8a90-1b2c3d4e5f60"
  },
  "step": "insider",
  "subject": "The part I only tell members",
  "recipients": [
    "me@example.com"
  ],
  "sent_to_owner": false,
  "sent": 1,
  "failed": 0
}
```

### 5. Check which arm a reader took

A run is the record of one subscriber's passage, and `steps` is the answer to "which way did this one go": one entry per step that reached the point of sending, so a step the run branched away from has none at all.

The run below took the member arm. `welcome` and `insider` are there, `best-of` and `nudge` are not, and `current_step` is null because `insider` pointed nowhere and that finished it. `status` is `completed`, which means it reached the end of its path rather than stopping short.

This is also how you tell a branch that works from one that is always taking the same side. If every run in the list has the same `steps`, the condition is matching the same way every time, which usually means it names a property the run cannot see or is missing its `required`.

`GET /runs/{run}` ([reference](https://api-reference.usecommune.dev/operation/operation-getautomationrun))

```sh
curl "https://api.usecommune.com/runs/77889900-aabb-4ccd-8eef-112233445566" \
  -H "Authorization: Bearer $COMMUNE_API_KEY" \
  -H "Commune-Version: 2026-08-26"
```

Response `200`:

```json
{
  "object": "automation_run",
  "id": "77889900-aabb-4ccd-8eef-112233445566",
  "automation": {
    "object": "automation",
    "id": "9c1e4b7a-2d35-4f61-8a90-1b2c3d4e5f60"
  },
  "subscriber": {
    "object": "subscriber",
    "id": "44556677-8899-4aa1-b2c3-d4e5f6071829"
  },
  "status": "completed",
  "current_step": null,
  "exit_reason": null,
  "attempts": 3,
  "steps": [
    {
      "step": "welcome",
      "status": "clicked",
      "failure_reason": null,
      "sent_at": "2026-10-05T08:00:01Z",
      "delivered_at": "2026-10-05T08:00:06Z",
      "opened_at": "2026-10-05T08:41:12Z",
      "clicked_at": "2026-10-05T08:42:03Z",
      "bounced_at": null,
      "complained_at": null
    },
    {
      "step": "insider",
      "status": "delivered",
      "failure_reason": null,
      "sent_at": "2026-10-08T08:00:02Z",
      "delivered_at": "2026-10-08T08:00:11Z",
      "opened_at": null,
      "clicked_at": null,
      "bounced_at": null,
      "complained_at": null
    }
  ],
  "next_run_at": null,
  "created_at": "2026-10-05T08:00:00Z",
  "completed_at": "2026-10-08T08:00:02Z"
}
```

### 6. Take an arm away again, and say where its readers go

A month later you decide the readers who never opened anything should get nothing more rather than a note about their spam folder. So `nudge` comes out, and `read-it`'s other arm points at nothing instead, which ends the run there.

**That edit is refused on its own.** A run sits on a step `id`, so removing a step people are standing on would leave those subscribers part way through the sequence with nothing to advance them and nothing reporting it. Without `on_removed_step` the answer is a `409` saying how many runs are waiting and on which steps.

`on_removed_step` is either `"exit"`, which ends those runs, or the `id` of a step in the graph you are sending, which is where they carry on from. A target that is not in the new graph is refused, because moving a run to a step that is also being removed would strand it again. It is ignored when the edit removes nothing anything is waiting on, and refused when the request sends no `steps` at all, since then it removes nothing.

The runs it ends get an `exit_reason` of `step_removed`, and each publishes `automation.run.exited`, so a consumer watching the sequence sees why rather than seeing a run go quiet.

**Pausing is the other way to stop an arm.** If what you want is to stop the whole sequence for a while rather than change it, pause it. A pause holds every waiting run exactly where it is and resumes from that step when you turn it back on, so nobody loses their place and nobody is emailed twice.

`PATCH /automations/{automation}` ([reference](https://api-reference.usecommune.dev/operation/operation-updateautomation))

```sh
curl -X PATCH "https://api.usecommune.com/automations/9c1e4b7a-2d35-4f61-8a90-1b2c3d4e5f60" \
  -H "Authorization: Bearer $COMMUNE_API_KEY" \
  -H "Commune-Version: 2026-08-26" \
  -H "Idempotency-Key: welcome-drop-nudge-2026-11-04" \
  -H "Content-Type: application/json" \
  -d '{
  "on_removed_step": "exit",
  "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 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": "member"
    },
    {
      "id": "member",
      "type": "condition",
      "schema": {
        "properties": {
          "subscriber": {
            "properties": {
              "tags": {
                "contains": {
                  "const": "aa11bb22-cc33-4d44-8e55-ff6677889900"
                }
              }
            },
            "required": [
              "tags"
            ]
          }
        },
        "required": [
          "subscriber"
        ]
      },
      "then": "insider",
      "else": "read-it"
    },
    {
      "id": "insider",
      "type": "send_email",
      "email": {
        "subject": "The part I only tell members",
        "body_markdown": "# The part I only tell members\n\nYou have been supporting this letter from early on, so here is the thing I\nleave out of the public archive: what I am working on next, and why.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    },
    {
      "id": "read-it",
      "type": "condition",
      "schema": {
        "properties": {
          "steps": {
            "properties": {
              "welcome": {
                "properties": {
                  "opened": {
                    "const": true
                  }
                },
                "required": [
                  "opened"
                ]
              }
            },
            "required": [
              "welcome"
            ]
          }
        },
        "required": [
          "steps"
        ]
      },
      "then": "best-of",
      "else": null
    },
    {
      "id": "best-of",
      "type": "send_email",
      "email": {
        "subject": "Three things worth reading",
        "body_markdown": "# Three things worth reading\n\nA few pieces that explain what this letter is about better than I can in a\nwelcome email.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    }
  ]
}'
```

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\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": "member",
      "then": null,
      "else": null,
      "duration": "P3D",
      "tag": null,
      "schema": null,
      "email": null
    },
    {
      "id": "member",
      "type": "condition",
      "next": null,
      "then": "insider",
      "else": "read-it",
      "duration": null,
      "tag": null,
      "schema": {
        "properties": {
          "subscriber": {
            "properties": {
              "tags": {
                "contains": {
                  "const": "aa11bb22-cc33-4d44-8e55-ff6677889900"
                }
              }
            },
            "required": [
              "tags"
            ]
          }
        },
        "required": [
          "subscriber"
        ]
      },
      "email": null
    },
    {
      "id": "insider",
      "type": "send_email",
      "next": null,
      "then": null,
      "else": null,
      "duration": null,
      "tag": null,
      "schema": null,
      "email": {
        "subject": "The part I only tell members",
        "preview_text": null,
        "body_markdown": "# The part I only tell members\n\nYou have been supporting this letter from early on, so here is the thing I\nleave out of the public archive: what I am working on next, and why.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    },
    {
      "id": "read-it",
      "type": "condition",
      "next": null,
      "then": "best-of",
      "else": null,
      "duration": null,
      "tag": null,
      "schema": {
        "properties": {
          "steps": {
            "properties": {
              "welcome": {
                "properties": {
                  "opened": {
                    "const": true
                  }
                },
                "required": [
                  "opened"
                ]
              }
            },
            "required": [
              "welcome"
            ]
          }
        },
        "required": [
          "steps"
        ]
      },
      "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\nA few pieces that explain what this letter is about better than I can in a\nwelcome email.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    }
  ],
  "created_at": "2026-10-01T09:00:00Z",
  "updated_at": "2026-11-04T09:12:00Z",
  "activated_at": "2026-10-02T11:16:00Z"
}
```

### All together

The edit in step 3 as a read-modify-write, which is the shape every change to a live graph takes. It reads the graph, splices the branch in, and sends the whole thing back. The steps it is keeping are carried across rather than retyped, so a body you have not looked at in a month cannot be dropped by accident.

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

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,
  });
  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 graph as it stands. `steps` on an update replaces it entirely, so
//    everything being kept has to be carried across.
const before = await api(`/automations/${AUTOMATION}`);
const keep = (id) => before.steps.find((step) => step.id === id);

// 2. The branch. `required` is the half that is easy to leave out: an absent
//    property matches everything in JSON Schema, so without it the condition
//    would take the same arm on every run.
const holdsTheTag = {
  properties: {
    subscriber: {
      properties: { tags: { contains: { const: TAG } } },
      required: ["tags"],
    },
  },
  required: ["subscriber"],
};

const openedTheFirst = {
  properties: {
    steps: {
      properties: {
        welcome: { properties: { opened: { const: true } }, required: ["opened"] },
      },
      required: ["welcome"],
    },
  },
  required: ["steps"],
};

// 3. The whole graph back, with the conditions spliced in. Each arm ends by
//    leaving `next` out, which ends the run: three endings, no node for any
//    of them, because there is no step type that means "end here".
const steps = [
  { ...keep("welcome"), next: "settle" },
  { ...keep("settle"), next: "member" },
  { id: "member", type: "condition", schema: holdsTheTag, then: "insider", else: "read-it" },
  {
    id: "insider",
    type: "send_email",
    email: {
      subject: "The part I only tell members",
      body_markdown: process.env.INSIDER_BODY,
    },
  },
  { id: "read-it", type: "condition", schema: openedTheFirst, then: "best-of", else: "nudge" },
  keep("best-of"),
  {
    id: "nudge",
    type: "send_email",
    email: {
      subject: "Did this land in the wrong folder?",
      body_markdown: process.env.NUDGE_BODY,
    },
  },
];

const after = await api(`/automations/${AUTOMATION}`, {
  method: "PATCH",
  idempotencyKey: "welcome-branch-v1",
  body: { steps },
});

console.log(`${after.name}: ${after.steps.length} steps, status ${after.status}.`);

// 4. Nobody has read the new emails rendered. Do that before anybody arrives.
for (const id of ["insider", "nudge"]) {
  const test = await api(`/automations/${after.id}/steps/${id}/send-test`, {
    method: "POST",
    idempotencyKey: `branch-test-${id}-v1`,
    body: { to: [process.env.REVIEW_ADDRESS] },
  });
  console.log(`${id}: sent ${test.sent}, failed ${test.failed}`);
}
```
