Automation run ended early Webhook

Add MCP server to your AI tool

Allow AI tools and LLMs to interact with the API documentation portal through MCP.

MCP server URL

https://api-reference.usecommune.dev/mcp

Standard setup for AI tools providing an mcp.json file

mcp.json
{
  "REST API MCP server": {
    "url": "https://api-reference.usecommune.dev/mcp"
  }
}

Close
POST https://webhook.example.com

A run stopped before the end of its graph. data.status says which of the two ways it stopped and data.reason says why.

exited is the ordinary case and is not an error: the subscriber unsubscribed, bounced or complained part way through a sequence, the creator archived the automation, or a live edit removed the step the run was sitting on. failed is a run Commune could not advance and has given up retrying, which is the one that wants a person.

One topic for both, rather than one per status, for the reason article.liked carries both directions: a consumer watching whether its creator's sequences finish needs every early ending, and splitting them would let it subscribe to half and believe it had them all. Route on data.status if the distinction matters.

data.reason is a short Commune-chosen code for an exited run (subscriber_unsubscribed, step_removed, automation_archived, sender_not_verified, and others; the set grows as the engine learns new ways to stop) and the last error text for a failed one. New codes are not a breaking change, so treat an unrecognised one as "ended, for a reason I do not handle" rather than as a parse failure.

No message is published for a run the loop guard refused, because no run was created and there would be no run_id to name. See automation.run.completed on how the guard reads actor.

Delivered as a single HTTPS POST to the consumer's registered endpoint, with the message as the JSON request body. Respond 2xx to acknowledge. Anything else, or a timeout, is retried with backoff, so acknowledge fast and do the work afterwards.

Headers

  • Commune-Version string Required

    API version the payload conforms to. Same value as api_version in the envelope.

  • Commune-Event-Id string(uuid) Required

    Same value as id in the envelope. The dedupe key.

  • Commune-Event-Type string Required

    Same value as type in the envelope. The topic name.

  • Commune-Signature string Required

    HMAC-SHA256 over Commune-Timestamp as Unix seconds, a literal ., then the raw request body, keyed by the endpoint's signing secret. Lowercase hex, carried as v0= followed by one or more comma separated digests (more than one while a secret is being rotated). The header's timestamp is an RFC 3339 instant, so convert it to Unix seconds before signing. Verify against the raw bytes before parsing the JSON. The webhooks guide has the full procedure.

  • Commune-Timestamp string(date-time) Required

    When this delivery attempt was made, as an RFC 3339 instant in UTC. It differs on every retry, and it IS covered by the signature, in Unix seconds, so a stale or altered one can be rejected. Enforce a tolerance window of a few minutes and dedupe on the event id as well, since a retry inside that window is legitimate.

  • Commune-Delivery-Attempt integer

    1 on the first attempt, incremented on each retry. Not sent today: do not rely on it, and be idempotent regardless. The same number is readable after the fact as attempt on a DeliveryAttempt, so a consumer that needs to tell a retry from a first delivery can ask instead of being told.

    Minimum value is 1.

application/json

Body Required

A run stopped before the end of its graph, deliberately or because Commune gave up advancing it.

  • id string(uuid) Required

    Unique id for this event. Stable across redeliveries, so it is the dedupe key. Assigned when the state change is recorded rather than when the message is dispatched.

  • type string Required

    The topic name, identical to the channel address. Route on this.

    Value is automation.run.exited.

  • api_version string Required

    The Commune-Version value this payload conforms to. Present on the message itself, not only on the request header, so an event persisted to a consumer's own store stays self-describing.

  • occurred_at string(date-time) Required

    RFC 3339 timestamp of the state change, not of the delivery attempt. Use it to order events, since delivery order is not guaranteed.

  • newsletter_id string(uuid) | null Required

    The newsletter the change belongs to. This is the tenant boundary: a consumer only ever receives events for newsletters its credential can read. Null only for events that are genuinely not newsletter-scoped, which none of the topics Commune publishes currently are.

  • actor object | null Required

    Who caused the change, and null when nobody in particular did.

    Populated on a change made through this API's write operations, where it is the credential, and on a change one of the newsletter's own automations made, where it is the run. Null on every other change: an edit a creator made in the product, an import arriving from a provider, a scheduled job, a delivery result reported by the sending provider. Null is therefore the common case and stays a legitimate value. Read it as "this change was attributed", never as "nothing caused this".

    It never names a person: an api_key actor is the credential and a system actor is an automation run.

    It answers "did this change come in through the API, and under which credential". It is not the field to drop the echo of your own write with: use idempotency_key, which you chose and therefore already know, whereas id here is Commune's own identifier for your credential and no operation reports it back to you. A creator can read that identifier in Commune, but it changes when the credential is replaced, so matching on it puts the loop back silently after a rotation.

    Additional properties are NOT allowed.

    Hide actor attributes Show actor attributes object | null
    • type string Required

      api_key for a change made through this API. system for a change one of the newsletter's own automations made, where id is the automation run and label names the automation: see automation.run.started for what that attribution is for. user is reserved for a change a named person made through an authenticated session and is not emitted.

      An OAuth access token reports as api_key as well. The two credentials are interchangeable everywhere else in this API, and a separate value here would be a distinction a consumer cannot act on.

      Values are user, api_key, or system.

    • id string Required

      Identifier of the actor within its type. For api_key it is the credential's own id and never its secret, and it is the same value that appears in a creator's list of credentials, so an event can be traced back to the integration that caused it.

    • label string | null

      Human-readable name for display. Best effort, may be null.

  • idempotency_key string | null Required

    The Idempotency-Key of the API write that caused this change, so a consumer can tie an event back to its own request and collapse the duplicates a retried write would otherwise produce.

    This is how a consumer that writes recognises its own work. Keep the keys you send, and skip any event carrying one of them. You chose the value, so you know it before the event arrives. actor cannot do this job, because Commune's id for your credential is not something this API reports to you.

    Null for every change that did not come in through this API, which is most of them: an edit made in Commune, an import, a scheduled job and a delivery result all have no originating request to key on.

    It reaches every endpoint registered for the newsletter, not only the one that made the write, so choose opaque keys such as UUIDs rather than keys carrying your own business identifiers.

    Maximum length is 255.

  • data object Required

    Event-specific body. Narrowed by each concrete event schema below.

    Hide data attributes Show data attributes object
    • run_id string(uuid) Required

      Commune's identifier for this run, and the value actor.id carries on this message and on every event this run's own actions cause.

    • automation_id string(uuid) Required

      The automation this run is a passage through.

    • automation_name string Required

      The automation's name as the creator last saved it, so a consumer can report the run without a second call. Renaming the automation does not rewrite the events already delivered.

    • trigger string Required

      The event topic the automation is built on, which is the topic of the event that started this run. One of the topics in this webhooks block: every topic Commune publishes is a trigger.

    • subscriber_id string(uuid) | null Required

      The person the run is about. Null when the run's trigger is not about a person, which is every topic outside the subscriber.* and delivery.* families. Such a run can read its event and wait, but it has nobody to email or tag.

    • event_id string(uuid) Required

      The envelope id of the event that started the run. The same value a consumer saw on that message, so a run can be tied back to the change that caused it. UNIQUE (automation_id, event_id) is what stops one event starting the same automation twice, so this pair is also the run's natural key.

    • chain_depth integer Required

      How many automations' actions led to this run. Zero for a run caused by a person or by Commune itself, one for a run started by what another automation did, and so on. Five is the limit: a sixth hop is refused rather than started, and so is any hop that would re-enter an automation already in the chain.

      The chain itself is not in the payload. actor.id names the run that caused the event this run started from, so a consumer that keeps these messages can walk the chain back one hop per message.

      Minimum value is 0.

    • started_at string(date-time) Required

      When the run was created. Equal to occurred_at on automation.run.started, and earlier than it on the other two.

    • completed_at string(date-time) Required

      When the run ended. Equal to occurred_at.

    • status string Required

      exited is an ordinary early ending and not an error: the subscriber left the list, the creator archived the automation, or an edit removed the step the run was on. failed is a run Commune could not advance and has stopped retrying.

      Values are exited or failed.

    • reason string | null Required

      A short Commune-chosen code when status is exited, and the last error text when it is failed. The set of codes grows as the engine learns new ways to stop, and a new one is not a breaking change, so treat an unrecognised value as "ended, for a reason I do not handle".

      Maximum length is 1000.

Responses

  • 200

    The consumer accepted the delivery. Any 2xx acknowledges the message and it is not sent again.

  • 4XX

    The consumer rejected the delivery. Handled exactly like a 5xx: the message is retried with backoff, because a rejection cannot be told apart from a consumer that is briefly misconfigured.

  • 5XX

    The consumer failed to handle the delivery. Retried with backoff. A timeout is the same case and is retried too.

POST automation.run.exited
Request examples
{
  "id": "018f2a93-8080-7000-8000-000000000081",
  "type": "automation.run.exited",
  "api_version": "2026-08-26",
  "occurred_at": "2026-08-29T14:22:10Z",
  "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-29T14:22:10Z",
    "status": "exited",
    "reason": "subscriber_unsubscribed"
  }
}
{
  "id": "018f2a93-9090-7000-8000-000000000091",
  "type": "automation.run.exited",
  "api_version": "2026-08-26",
  "occurred_at": "2026-08-29T16:40:00Z",
  "newsletter_id": "7d3f1c02-58a1-4a4e-9a0b-2f6d1c9e4411",
  "actor": {
    "type": "system",
    "id": "3c8d5e2a-7f14-4099-8b32-6d7e8f9a0b1c",
    "label": "automation: Welcome sequence"
  },
  "idempotency_key": null,
  "data": {
    "run_id": "3c8d5e2a-7f14-4099-8b32-6d7e8f9a0b1c",
    "automation_id": "9c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f",
    "automation_name": "Welcome sequence",
    "trigger": "subscriber.created",
    "subscriber_id": "6f7a8b9c-0d1e-4f2a-8b3c-4d5e6f7a8b9c",
    "event_id": "018f2a93-6060-7000-8000-000000000061",
    "chain_depth": 0,
    "started_at": "2026-08-29T09:00:01Z",
    "completed_at": "2026-08-29T16:40:00Z",
    "status": "failed",
    "reason": "send_blocked:fair_use_limit"
  }
}