Tag a reader the moment they act
A segment you maintain by hand is out of date the day after you build it. Commune can watch for the thing that puts somebody in a group, move the tag the moment it happens, and then say something to them a day or two later, so the segment and the follow-up are the same piece of work.
Before you start
An automation that moves a tag causes an event, because moving a tag publishes one. Commune refuses to start a run whose chain already contains that automation, so a sequence triggered by the tag it applies does not loop. It also means the second automation you were about to build on that tag may never fire, and the last step of this page is how you tell.
Your agent can make the tag, write the automation and leave it off until you have read the email. This route writes. It creates a tag and a draft automation on your newsletter, and the tag it creates is visible to you in Commune straight away.
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.
In my Commune newsletter, make a tag called "Going quiet", then write me a draft automation: when a reader's engagement drops a band, add that tag, wait two days, and send them a short note asking whether they still want the letter. Only when their engagement drops, not when it rises.
Allow somebody to go through it more than once. Do not turn it on, and send me a test copy of the email first.
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.
commune_readReads the trigger catalog, which is what says that this topic's events are about a subscriber and which properties a filter may name under it. A filter on a property the payload does not carry would match every event.
commune_writeCreates the tag, writes the graph as a draft, sends the test copy, and turns the automation on when you ask. A draft sends nothing.
who_is_drifting_awayAnswers how many readers are cooling off right now, which is the size of the group this automation will start reaching.
What comes back
Made the tag and wrote the automation. It is off.
| Piece | What it is |
|---|---|
| Tag | "Going quiet", 0 readers so far |
| Trigger | A reader crossing the engagement ladder |
| Filter | Downwards only, so a reader getting *more* engaged hears nothing |
| Steps | Add the tag, wait 2 days, send one note |
- Re-entry is on, so somebody who cools off, comes back and cools off again a year later gets the note again.
- 31 readers are cooling off right now. They will not be tagged retrospectively: the automation reacts to the next crossing, not to the backlog.
- One thing to decide before you turn it on: adding a tag publishes an event of its own, so if you later build something that triggers on this tag, Commune will refuse to run it from this automation's own tagging rather than loop.
- The test copy is in your inbox.
An example. Yours comes from your own newsletter.
Worth asking next
- Turn it on, and tell me next week how many readers it reached.
- Who is holding the Going quiet tag right now?
- Pause it for December, and resume it in January without losing anybody's place.
Tips
- Say which direction you mean. The event fires on every crossing of the ladder, both ways, and an automation with no filter on it emails people for reading more.
- It reacts to the next crossing rather than to the readers who are already quiet. If you want to reach the existing backlog once, that is a one-off list and a one-off email, not a standing instruction.
- Ask for the tag's name back. A tag is matched in a condition by id, so a second automation that wants to read this one's tag needs the id rather than the name.
Four requests and an event. Make the tag, write the graph with a filter so it reacts to the readers you actually mean, turn it on, and ask who is in the group. The event is the one that explains why the next automation you build on this tag may never fire.
Before you start
- An API key with
sending: write,audience: write, exported asCOMMUNE_API_KEY. - A newsletter Commune sends the email for, with a verified sending address. The tagging step would work without one; the email step is what the activation checks.
- An address you can read, for the test copy.
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
Make the tag the automation will apply
A tag step names a tag by
id, so the tag has to exist first. A step naming a tag that has been deleted is refused: it would do nothing, every run, forever.The name has to be unique among the newsletter's live tags, so creating one that already exists answers
409rather than quietly handing you the existing one.known_subscriber_countstarts at zero and is what you will watch.It is a tag like any other: you can see it in Commune, apply it by hand, and send an article to it. The automation is just another thing that moves it.
cURLcurl -X POST "https://api.usecommune.com/newsletters/example-letter/tags" \ -H "Authorization: Bearer $COMMUNE_API_KEY" \ -H "Commune-Version: 2026-08-26" \ -H "Idempotency-Key: going-quiet-tag-2026-10-09" \ -H "Content-Type: application/json" \ -d '{ "name": "Going quiet" }'Response 201{ "object": "tag", "id": "cc33dd44-ee55-4f66-8a77-b8c9d0e1f203", "newsletter": { "object": "newsletter", "id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411" }, "name": "Going quiet", "known_subscriber_count": 0, "is_room": false, "retired": false, "retired_at": null, "created_at": "2026-10-09T15:18:00Z" } - 2
Write it, filtered to the readers you actually mean
The trigger is a reader crossing a boundary in the newsletter's engagement ladder. That event fires in both directions, so an automation on it with no filter would send this email to somebody who had just started reading everything.
trigger_filteris what stops that. It is a JSON Schema document in the same closed subset aconditionstep uses, and it decides whether an event starts a run at all. The paths in it are the event payload's own, not the three-branch context aconditionis evaluated against, because a filter runs before there is a run to have a context.directionispromotedordemoted, and this one keeps the demotions.requiredis doing real work there. An absent property says nothing in JSON Schema, sopropertieson its own would also match a payload carrying nodirectionat all.The graph is three steps wired one to the next, and the last one wires nothing, which is what ends the run.
mark-quietmoves the tag on the subscriber the run is about.settleparks the run for two days, written as an ISO 8601 duration: there is no time-of-day field, because Commune does not know a subscriber's time zone and a time of day would be interpreted in UTC.reach-outsends the email, with the unsubscribe link and{{ address }}that Commune refuses to send without.reentry: truebecause this is not a welcome. A reader can cool off, come back, and cool off again two years later, and the note is worth sending both times. The default isfalse, which means once ever.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: going-quiet-automation-2026-10-09" \ -H "Content-Type: application/json" \ -d '{ "name": "Reach out when a reader cools off", "trigger": "subscriber.status_changed", "trigger_filter": { "properties": { "direction": { "const": "demoted" } }, "required": [ "direction" ] }, "reentry": true, "steps": [ { "id": "mark-quiet", "type": "add_tag", "tag": "cc33dd44-ee55-4f66-8a77-b8c9d0e1f203", "next": "settle" }, { "id": "settle", "type": "wait", "duration": "P2D", "next": "reach-out" }, { "id": "reach-out", "type": "send_email", "email": { "subject": "Still want this, {{ subscriber.first_name }}?", "preview_text": "One line back is enough.", "body_markdown": "# Still want this?\n\nYou used to read most of these and have not opened one in a while, which\nis fine. If the letter has drifted away from what you signed up for, tell\nme. A one-line reply is worth more to me than any survey.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n" } } ] }'Response 201{ "object": "automation", "id": "2d4f6a81-3b5c-4d7e-8f90-a1b2c3d4e5f6", "newsletter": { "object": "newsletter", "id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411" }, "name": "Reach out when a reader cools off", "status": "draft", "trigger": "subscriber.status_changed", "trigger_filter": { "properties": { "direction": { "const": "demoted" } }, "required": [ "direction" ] }, "reentry": true, "steps": [ { "id": "mark-quiet", "type": "add_tag", "next": "settle", "then": null, "else": null, "duration": null, "tag": "cc33dd44-ee55-4f66-8a77-b8c9d0e1f203", "schema": null, "email": null }, { "id": "settle", "type": "wait", "next": "reach-out", "then": null, "else": null, "duration": "P2D", "tag": null, "schema": null, "email": null }, { "id": "reach-out", "type": "send_email", "next": null, "then": null, "else": null, "duration": null, "tag": null, "schema": null, "email": { "subject": "Still want this, {{ subscriber.first_name }}?", "preview_text": "One line back is enough.", "body_markdown": "# Still want this?\n\nYou used to read most of these and have not opened one in a while, which\nis fine. If the letter has drifted away from what you signed up for, tell\nme. A one-line reply is worth more to me than any survey.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n" } } ], "created_at": "2026-10-09T15:20:00Z", "updated_at": "2026-10-09T15:20:00Z", "activated_at": null } - 3
Turn it on
Read a test copy of
reach-outfirst. The step is the same one Welcome every new reader walks through, and the reason is the same: there is no moment at which anybody watches an automation email go out, so without a test copy the first person to see it rendered is a reader you are already at risk of losing.Every gate runs again here on the stored automation: the graph, the filter, the email's footer, every image in it, and the newsletter's own ability to send. Each is a
422naming what is wrong. The newsletter checks apply only because this graph sends; an automation that only moved the tag would be turned on without them.It reacts to the next crossing, not to the backlog. Readers who were already cooling off before you turned this on are not tagged retrospectively. If you want to reach them once, that is a list and a one-off email rather than a standing instruction, and Win back readers is that page.
This installs a standing instruction
From the moment it returns, Commune emails whoever the trigger names, as often as it fires, with no further request from anybody. It is charged to a daily cap counted per newsletter, reported as a
429whoseRetry-Afteris measured in hours rather than seconds.cURLcurl -X POST "https://api.usecommune.com/automations/2d4f6a81-3b5c-4d7e-8f90-a1b2c3d4e5f6/activate" \ -H "Authorization: Bearer $COMMUNE_API_KEY" \ -H "Commune-Version: 2026-08-26" \ -H "Idempotency-Key: going-quiet-activate-2026-10-09"Response 200{ "object": "automation", "id": "2d4f6a81-3b5c-4d7e-8f90-a1b2c3d4e5f6", "newsletter": { "object": "newsletter", "id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411" }, "name": "Reach out when a reader cools off", "status": "active", "trigger": "subscriber.status_changed", "trigger_filter": { "properties": { "direction": { "const": "demoted" } }, "required": [ "direction" ] }, "reentry": true, "steps": [ { "id": "mark-quiet", "type": "add_tag", "next": "settle", "then": null, "else": null, "duration": null, "tag": "cc33dd44-ee55-4f66-8a77-b8c9d0e1f203", "schema": null, "email": null }, { "id": "settle", "type": "wait", "next": "reach-out", "then": null, "else": null, "duration": "P2D", "tag": null, "schema": null, "email": null }, { "id": "reach-out", "type": "send_email", "next": null, "then": null, "else": null, "duration": null, "tag": null, "schema": null, "email": { "subject": "Still want this, {{ subscriber.first_name }}?", "preview_text": "One line back is enough.", "body_markdown": "# Still want this?\n\nYou used to read most of these and have not opened one in a while, which\nis fine. If the letter has drifted away from what you signed up for, tell\nme. A one-line reply is worth more to me than any survey.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n" } } ], "created_at": "2026-10-09T15:20:00Z", "updated_at": "2026-10-09T15:20:00Z", "activated_at": "2026-10-09T15:41:00Z" } - 4
Watch the tag move, and watch for the loop
Every tag the automation applies publishes
subscriber.tagged, exactly as a tag applied by hand does, with one difference worth all of this paragraph:actornames the run that caused it.That attribution is what the loop guard reads. A run started by this event inherits this run's chain plus this run's own automation, and two things are then refused before a run exists: a chain that already contains that automation, which is a cycle of any length; and a chain already five automations deep. So an automation triggered by
subscriber.taggedwhose steps add a tag loops immediately on paper and is refused in practice.Nothing is published for a run the guard refused, because no run was created and there would be no run id to name. A refusal therefore looks exactly like an automation nobody switched on. If you build a second automation on this tag and it never fires, this is the first thing to check, before the trigger, the filter and the key.
data.chain_depthon everyautomation.run.*message says how many automations' actions led to that run: zero for one a person caused, one for a run started by what another automation did.Commune posts
subscriber.taggedto your endpoint:event subscriber.tagged{ "id": "018f2a94-1010-7000-8000-000000000101", "type": "subscriber.tagged", "api_version": "2026-08-26", "occurred_at": "2026-10-10T06:12:00Z", "newsletter_id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411", "actor": { "type": "system", "id": "4e5f6a7b-8c9d-4e0f-8a1b-2c3d4e5f6a7b", "label": "automation: Reach out when a reader cools off" }, "idempotency_key": null, "data": { "subscriber_id": "33445566-7788-4990-a1b2-c3d4e5f60718", "email": "[email protected]", "tag_id": "cc33dd44-ee55-4f66-8a77-b8c9d0e1f203", "tag_name": "Going quiet", "direction": "assigned" } } - 5
Ask who is in the group
The tag is an ordinary tag, so this is the ordinary question.
?tag=takes the tag'sidand?status=subscribedkeeps the people the newsletter can still reach, which is the filter you want on a group defined by having gone quiet: an unsubscribe does not remove somebody's tags.This needs
audience: readon top of the sending permission, because it returns email addresses. The automation's own runs do not: a run is read undersendingand carries no address at all, which is why there are two permissions on this page rather than one.To see where each of those readers is in the sequence instead, list the automation's runs.
?status=waitingis the readers sitting on the two-day wait, which is the group that has been tagged and not yet written to.cURLcurl "https://api.usecommune.com/newsletters/example-letter/subscribers?tag=cc33dd44-ee55-4f66-8a77-b8c9d0e1f203&status=subscribed&limit=100" \ -H "Authorization: Bearer $COMMUNE_API_KEY" \ -H "Commune-Version: 2026-08-26"Response 200{ "object": "list", "data": [ { "object": "subscriber", "id": "33445566-7788-4990-a1b2-c3d4e5f60718", "newsletter": { "object": "newsletter", "id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411" }, "user": { "object": "user", "id": "usr_2xR8kQ4mN7pL1vB9" }, "email": "[email protected]", "status": "subscribed", "source": "commune", "tags": [ { "object": "tag", "id": "cc33dd44-ee55-4f66-8a77-b8c9d0e1f203" } ], "created_at": "2026-02-14T10:31:07Z", "synced_at": null } ], "pagination": { "has_more": false, "next_cursor": null } } - 6
Stop it without losing anybody's place
A pause holds every waiting run exactly where it is. A reader one day into the two-day wait stays on that step and resumes from it when you turn the automation back on, so there is no window in which a paused automation still sends and no drain to wait out.
That is the difference from archiving, which is what deleting an automation does. An archive ends the runs it was in the middle of, with an
exit_reasonsaying so, because an archive is not coming back and a run holding its place on a graph nobody will activate again is a subscriber stuck part way through forever. There is no way back from archived: write a new automation instead.Pausing a paused automation succeeds and changes nothing. A draft or an archived one is refused, because neither is sending and there is nothing to pause.
The tag stays where it is either way. Pausing stops the automation, not the segment, so the readers it has already tagged are still in the group and still reachable by hand.
cURLcurl -X POST "https://api.usecommune.com/automations/2d4f6a81-3b5c-4d7e-8f90-a1b2c3d4e5f6/pause" \ -H "Authorization: Bearer $COMMUNE_API_KEY" \ -H "Commune-Version: 2026-08-26" \ -H "Idempotency-Key: going-quiet-pause-2026-12-01"Response 200{ "object": "automation", "id": "2d4f6a81-3b5c-4d7e-8f90-a1b2c3d4e5f6", "newsletter": { "object": "newsletter", "id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411" }, "name": "Reach out when a reader cools off", "status": "paused", "trigger": "subscriber.status_changed", "trigger_filter": { "properties": { "direction": { "const": "demoted" } }, "required": [ "direction" ] }, "reentry": true, "steps": [ { "id": "mark-quiet", "type": "add_tag", "next": "settle", "then": null, "else": null, "duration": null, "tag": "cc33dd44-ee55-4f66-8a77-b8c9d0e1f203", "schema": null, "email": null }, { "id": "settle", "type": "wait", "next": "reach-out", "then": null, "else": null, "duration": "P2D", "tag": null, "schema": null, "email": null }, { "id": "reach-out", "type": "send_email", "next": null, "then": null, "else": null, "duration": null, "tag": null, "schema": null, "email": { "subject": "Still want this, {{ subscriber.first_name }}?", "preview_text": "One line back is enough.", "body_markdown": "# Still want this?\n\nYou used to read most of these and have not opened one in a while, which\nis fine. If the letter has drifted away from what you signed up for, tell\nme. A one-line reply is worth more to me than any survey.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n" } } ], "created_at": "2026-10-09T15:20:00Z", "updated_at": "2026-10-09T15:20:00Z", "activated_at": "2026-10-09T15:41:00Z" }
All together
The tag and the automation as one file, stopping before activation. It is written so a second run changes nothing: the tag is reused if it already exists, and the idempotency keys are fixed rather than generated, so a retry after a timeout cannot leave two automations behind.
const BASE = "https://api.usecommune.com";
const VERSION = "2026-08-26";
const KEY = process.env.COMMUNE_API_KEY;
const NEWSLETTER = process.env.COMMUNE_NEWSLETTER;
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;
}
const TAG_NAME = "Going quiet";
// 1. The tag. A name has to be unique among the live tags, so a second run
// answers 409 rather than making a duplicate; reuse what is there.
const { data: tags } = await api(`/newsletters/${NEWSLETTER}/tags`);
const existing = tags.find((tag) => tag.name === TAG_NAME);
const tag =
existing ??
(await api(`/newsletters/${NEWSLETTER}/tags`, {
method: "POST",
idempotencyKey: "going-quiet-tag-v1",
body: { name: TAG_NAME },
}));
console.log(`Tag ${tag.id} (${tag.known_subscriber_count} readers).`);
// 2. The automation. The filter runs against the event's own payload before
// a run exists, which is why it names `direction` rather than anything
// under `subscriber`. Without `required`, a payload with no `direction`
// would match: an absent property says nothing in JSON Schema.
const draft = await api(`/newsletters/${NEWSLETTER}/automations`, {
method: "POST",
idempotencyKey: "going-quiet-automation-v1",
body: {
name: "Reach out when a reader cools off",
trigger: "subscriber.status_changed",
trigger_filter: {
properties: { direction: { const: "demoted" } },
required: ["direction"],
},
reentry: true,
steps: [
{ id: "mark-quiet", type: "add_tag", tag: tag.id, next: "settle" },
{ id: "settle", type: "wait", duration: "P2D", next: "reach-out" },
{
id: "reach-out",
type: "send_email",
email: {
subject: "Still want this, {{ subscriber.first_name }}?",
preview_text: "One line back is enough.",
body_markdown: process.env.REACH_OUT_BODY,
},
},
],
},
});
// 3. Read it before a reader does.
const test = await api(`/automations/${draft.id}/steps/reach-out/send-test`, {
method: "POST",
idempotencyKey: "going-quiet-test-v1",
body: { to: [process.env.REVIEW_ADDRESS] },
});
console.log(`Test copy: sent ${test.sent}, failed ${test.failed}.`);
console.log(
`Draft ${draft.id} is written and NOT sending. When you are happy:\n` +
` POST ${BASE}/automations/${draft.id}/activate`,
);Next use cases
See all- API
Bring the conversation to your site
Show each issue's discussion wherever your readers already are.
Read the use case - AgentAPI
Get a pulse on your newsletter in plain words
Ask how things are going and get an answer, not a dashboard.
Read the use case - AgentAPI
Build your website with your agent
Describe the site you want. Your agent writes it, and Commune hosts it.
Read the use case