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.
Before you start
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.
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 Commune's MCP server to your agent, then approve the consent screen and pick your newsletter. Nothing else to install.
claude mcp add --transport http commune https://api.usecommune.com/mcpAsk
One prompt does the whole job. Paste it as it is, or change the parts that are yours.
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.
What it uses
Your agent picks these of Commune's tools to answer. You do not name them; they are here so you know where every part of the answer comes from.
which_newsletters_can_i_reachLists the newsletters this connection was granted, so the draft lands on the one you meant.
commune_readReads 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_writeWrites 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
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.
An example. Yours comes from your own newsletter.
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.
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 with
sending: write, exported asCOMMUNE_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.
Run it in your client
Every request below, in order, as a collection. Import the file into Postman, Insomnia, Bruno, Yaak or Hoppscotch, set apiKey, and send them one by one.
Every request sends Authorization: Bearer $COMMUNE_API_KEY and Commune-Version: 2026-08-26.
- 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(orid, either works in a path): the automation is written under it.cURLcurl "https://api.usecommune.com/newsletters" \ -H "Authorization: Bearer $COMMUNE_API_KEY" \ -H "Commune-Version: 2026-08-26"Response 200{ "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.createdis the one a welcome sequence waits for, andcarries_subscriberistrue, which is what makes asend_emailstep 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.
cURLcurl "https://api.usecommune.com/automation-triggers?limit=100" \ -H "Authorization: Bearer $COMMUNE_API_KEY" \ -H "Commune-Version: 2026-08-26"Response 200{ "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
stepsis every step the sequence will have, and every edge between them is written down:welcomenamessettle,settlenamesbest-of, andbest-ofnames nothing, so the run ends there. Anextleft out is not "the next step", it is no step, which ends the run (it defaulted to the following entry until2026-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 withmissing_footerrather than sent. Every image in a body is probed too, because an inbox fetches images fresh when the reader opens the email.reentry: falsemeans 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.cURLcurl -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{ "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'll 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
toout and the copy goes to the address on the account this credential belongs to, which is whyrecipientscan come back empty withsent_to_owner: true.Repeat it for
best-of. The step id in the path is the one you chose when you wrote the graph.cURLcurl -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": [ "[email protected]", "[email protected]" ] }'Response 200{ "object": "automation_step_test", "automation": { "object": "automation", "id": "9c1e4b7a-2d35-4f61-8a90-1b2c3d4e5f60" }, "step": "welcome", "subject": "Welcome, Alex", "recipients": [ "[email protected]", "[email protected]" ], "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
422naming 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
429that is not one of theRateLimit-Policybudgets: itsRetry-Afteris 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
activesucceeds 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.
cURLcurl -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{ "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.
statusandcurrent_stepsay where they are,next_run_atsays when they move, andstepsis what each email did for that one person: sent, delivered, opened, clicked, bounced, complained, each with its own time.?status=waitingis 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
sendingand an address belongs toaudience, sosubscriberis a reference that?expand=subscriberdeliberately will not inflate. Resolve it with the subscriber endpoint if you need the address, under a key that holdsaudience: read.cURLcurl "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{ "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.startedwhen somebody enters, and then exactly one ofautomation.run.completedorautomation.run.exited.automation.run.exitedis the interesting one. It carries both the ordinary early endings and the failures, underdata.status:exitedis 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;failedis a run Commune could not advance and has given up on, which is the one that wants a person.actornames 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_depthis 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.
Commune posts
automation.run.completedto your endpoint:event automation.run.completed{ "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.
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`,
);Next use cases
See all- AgentAPI
Send a different email to different readers
One sequence, two paths: what you tell a long-standing member is not what you tell somebody who has not opened anything yet.
Read the use case - AgentAPI
Tag a reader the moment they act
Build the segment while people earn it, and say something to them while it still matters.
Read the use case - API
Bring the conversation to your site
Show each issue's discussion wherever your readers already are.
Read the use case