{
  "info": {
    "name": "Commune: Send a different email to different readers",
    "description": "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.\n\nThe walkthrough: https://usecommune.dev/use-cases/branch-a-sequence\n\nSet the `apiKey` variable to a key from https://usecommune.com/settings/api-keys, then run the requests in order.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{apiKey}}",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://api.usecommune.com"
    },
    {
      "key": "apiKey",
      "value": ""
    },
    {
      "key": "version",
      "value": "2026-08-26"
    },
    {
      "key": "automation",
      "value": "9c1e4b7a-2d35-4f61-8a90-1b2c3d4e5f60"
    },
    {
      "key": "newsletter",
      "value": "example-letter"
    },
    {
      "key": "step",
      "value": "insider"
    },
    {
      "key": "run",
      "value": "77889900-aabb-4ccd-8eef-112233445566"
    }
  ],
  "item": [
    {
      "name": "1. Read the graph you already have",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Commune-Version",
            "value": "{{version}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/automations/{{automation}}",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "automations",
            "{{automation}}"
          ]
        },
        "description": "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.\n\nThe `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.\n\nEvery 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."
      }
    },
    {
      "name": "2. Find the tag you want to branch on",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Commune-Version",
            "value": "{{version}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/newsletters/{{newsletter}}/tags",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "newsletters",
            "{{newsletter}}",
            "tags"
          ]
        },
        "description": "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.\n\n`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.\n\nRetired 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."
      }
    },
    {
      "name": "3. Send the graph back with the branch in it",
      "request": {
        "method": "PATCH",
        "header": [
          {
            "key": "Commune-Version",
            "value": "{{version}}"
          },
          {
            "key": "Idempotency-Key",
            "value": "{{$guid}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/automations/{{automation}}",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "automations",
            "{{automation}}"
          ]
        },
        "body": {
          "mode": "raw",
          "raw": "{\n  \"steps\": [\n    {\n      \"id\": \"welcome\",\n      \"type\": \"send_email\",\n      \"next\": \"settle\",\n      \"email\": {\n        \"subject\": \"Welcome, {{ subscriber.first_name }}\",\n        \"preview_text\": \"Here is where to start.\",\n        \"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\"\n      }\n    },\n    {\n      \"id\": \"settle\",\n      \"type\": \"wait\",\n      \"duration\": \"P3D\",\n      \"next\": \"member\"\n    },\n    {\n      \"id\": \"member\",\n      \"type\": \"condition\",\n      \"schema\": {\n        \"properties\": {\n          \"subscriber\": {\n            \"properties\": {\n              \"tags\": {\n                \"contains\": {\n                  \"const\": \"aa11bb22-cc33-4d44-8e55-ff6677889900\"\n                }\n              }\n            },\n            \"required\": [\n              \"tags\"\n            ]\n          }\n        },\n        \"required\": [\n          \"subscriber\"\n        ]\n      },\n      \"then\": \"insider\",\n      \"else\": \"read-it\"\n    },\n    {\n      \"id\": \"insider\",\n      \"type\": \"send_email\",\n      \"email\": {\n        \"subject\": \"The part I only tell members\",\n        \"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\"\n      }\n    },\n    {\n      \"id\": \"read-it\",\n      \"type\": \"condition\",\n      \"schema\": {\n        \"properties\": {\n          \"steps\": {\n            \"properties\": {\n              \"welcome\": {\n                \"properties\": {\n                  \"opened\": {\n                    \"const\": true\n                  }\n                },\n                \"required\": [\n                  \"opened\"\n                ]\n              }\n            },\n            \"required\": [\n              \"welcome\"\n            ]\n          }\n        },\n        \"required\": [\n          \"steps\"\n        ]\n      },\n      \"then\": \"best-of\",\n      \"else\": \"nudge\"\n    },\n    {\n      \"id\": \"best-of\",\n      \"type\": \"send_email\",\n      \"email\": {\n        \"subject\": \"Three things worth reading\",\n        \"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\"\n      }\n    },\n    {\n      \"id\": \"nudge\",\n      \"type\": \"send_email\",\n      \"email\": {\n        \"subject\": \"Did this land in the wrong folder?\",\n        \"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\"\n      }\n    }\n  ]\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "description": "Two conditions, and three endings.\n\n`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.\n\n**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.\n\n**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.\n\nThe 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.\n\n**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."
      }
    },
    {
      "name": "4. Look at the emails the new arms send",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Commune-Version",
            "value": "{{version}}"
          },
          {
            "key": "Idempotency-Key",
            "value": "{{$guid}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/automations/{{automation}}/steps/{{step}}/send-test",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "automations",
            "{{automation}}",
            "steps",
            "{{step}}",
            "send-test"
          ]
        },
        "body": {
          "mode": "raw",
          "raw": "{\n  \"to\": [\n    \"me@example.com\"\n  ]\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "description": "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.\n\nIt 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`."
      }
    },
    {
      "name": "5. Check which arm a reader took",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Commune-Version",
            "value": "{{version}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/runs/{{run}}",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "runs",
            "{{run}}"
          ]
        },
        "description": "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.\n\nThe 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.\n\nThis 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`."
      }
    },
    {
      "name": "6. Take an arm away again, and say where its readers go",
      "request": {
        "method": "PATCH",
        "header": [
          {
            "key": "Commune-Version",
            "value": "{{version}}"
          },
          {
            "key": "Idempotency-Key",
            "value": "{{$guid}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/automations/{{automation}}",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "automations",
            "{{automation}}"
          ]
        },
        "body": {
          "mode": "raw",
          "raw": "{\n  \"on_removed_step\": \"exit\",\n  \"steps\": [\n    {\n      \"id\": \"welcome\",\n      \"type\": \"send_email\",\n      \"next\": \"settle\",\n      \"email\": {\n        \"subject\": \"Welcome, {{ subscriber.first_name }}\",\n        \"preview_text\": \"Here is where to start.\",\n        \"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\"\n      }\n    },\n    {\n      \"id\": \"settle\",\n      \"type\": \"wait\",\n      \"duration\": \"P3D\",\n      \"next\": \"member\"\n    },\n    {\n      \"id\": \"member\",\n      \"type\": \"condition\",\n      \"schema\": {\n        \"properties\": {\n          \"subscriber\": {\n            \"properties\": {\n              \"tags\": {\n                \"contains\": {\n                  \"const\": \"aa11bb22-cc33-4d44-8e55-ff6677889900\"\n                }\n              }\n            },\n            \"required\": [\n              \"tags\"\n            ]\n          }\n        },\n        \"required\": [\n          \"subscriber\"\n        ]\n      },\n      \"then\": \"insider\",\n      \"else\": \"read-it\"\n    },\n    {\n      \"id\": \"insider\",\n      \"type\": \"send_email\",\n      \"email\": {\n        \"subject\": \"The part I only tell members\",\n        \"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\"\n      }\n    },\n    {\n      \"id\": \"read-it\",\n      \"type\": \"condition\",\n      \"schema\": {\n        \"properties\": {\n          \"steps\": {\n            \"properties\": {\n              \"welcome\": {\n                \"properties\": {\n                  \"opened\": {\n                    \"const\": true\n                  }\n                },\n                \"required\": [\n                  \"opened\"\n                ]\n              }\n            },\n            \"required\": [\n              \"welcome\"\n            ]\n          }\n        },\n        \"required\": [\n          \"steps\"\n        ]\n      },\n      \"then\": \"best-of\",\n      \"else\": null\n    },\n    {\n      \"id\": \"best-of\",\n      \"type\": \"send_email\",\n      \"email\": {\n        \"subject\": \"Three things worth reading\",\n        \"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\"\n      }\n    }\n  ]\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "description": "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.\n\n**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.\n\n`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.\n\nThe 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."
      }
    }
  ]
}
