Skip to main content

Messages and Signals

Messages and signals let process instances communicate — with each other, with starting events that haven't fired yet, and with anything outside the engine that can speak HTTP. They look superficially similar in the modeler, but their delivery semantics are very different and they're not interchangeable.

AspectMessageSignal
DeliveryPoint-to-point — one publish wakes at most one subscriberBroadcast — one publish wakes every subscriber
SelectionName + correlation keysName only
Late subscribers see buffered ones?Yes, until the buffer entry is consumed or expiresYes, until the buffer entry expires
Default buffer TTL1 hour24 hours
Used forRequest/response, awaiting an external system, addressing a specific instanceAbort, mass notification, fanning out a fact to whoever cares

Modeling

Both are declared as top-level definitions in the <definitions> root:

<!-- Message used to correlate to an existing instance — REQUIRES correlationKey. -->
<bpmn:message id="Msg_Payment" name="PaymentReceived">
<bpmn:extensionElements>
<zeebe:subscription correlationKey="=orderId" />
<quantum:ttl>PT1H</quantum:ttl>
</bpmn:extensionElements>
</bpmn:message>

<!-- Message used to start new instances — correlationKey OPTIONAL. -->
<bpmn:message id="Msg_OrderCreated" name="OrderCreated"/>

<bpmn:signal id="Sig_Abort" name="AbortRequested">
<bpmn:extensionElements>
<quantum:ttl>PT30M</quantum:ttl>
</bpmn:extensionElements>
</bpmn:signal>
Strict correlation rule (Zeebe-style)

Every <bpmn:message> plays exactly one role in your model:

  1. Source for a process-root start event — creates new instances. correlationKey is OPTIONAL.
  2. Anything else (intermediate catch, boundary, event-subprocess start, receive task, intermediate throw, end event) — correlates within an instance. correlationKey is REQUIRED.

Mixing the two — using the same <bpmn:message> as both a start trigger AND a catch-side correlator — is rejected at deploy time. Split into two separate <bpmn:message> definitions.

See Global definitions for the full attribute reference.

The element pages cover all the catch and throw positions where messages and signals can be used:

PositionMessage?Signal?Correlation required?
Process start event (new instance)optional
Event sub-process startrequired
Intermediate catchrequired
Intermediate throwrequired
Boundary event (interrupting and non-interrupting)required
End eventrequired
Receive taskrequired
Send taskrequired

For per-position attributes, see Events.


Messages

A message is delivered to exactly one subscriber. If several catches are subscribed when a publish arrives, the engine picks one and the rest stay registered for later publishes.

Correlation

A message subscription matches a publish when:

  1. The names match (the resolved name from the <bpmn:message> definition).
  2. The subscriber's correlation keys are a superset of the publisher's keys.

Containment is recursive — nested objects work the same way. A subscription whose key resolved to { orderId: 123, region: "EU" } matches a publish that carries { orderId: 123 }, but not the other way around. Treat the publisher's keys as a filter on the subscriber population.

Matching is type-sensitive: a subscription whose correlation key resolves to the number 123 does not match a publish that sends the string "123". Watch this when one side reads a JSON payload and the other reads a typed variable.

Two correlation patterns

Both patterns use the same <zeebe:subscription correlationKey> shape — the only difference is which FEEL expression you write.

Same-workflow (self-correlation)

To address the publishing instance only, use the auto-injected quantum.workflowId:

<bpmn:message id="Msg_Resume" name="ResumeAfterPause">
<bpmn:extensionElements>
<zeebe:subscription correlationKey="=quantum.workflowId" />
</bpmn:extensionElements>
</bpmn:message>
<!-- elsewhere in the same process -->
<bpmn:intermediateThrowEvent id="throw-resume">
<bpmn:messageEventDefinition messageRef="Msg_Resume" />
</bpmn:intermediateThrowEvent>

<bpmn:intermediateCatchEvent id="catch-resume">
<bpmn:messageEventDefinition messageRef="Msg_Resume" />
</bpmn:intermediateCatchEvent>

quantum.workflowId is the running instance's unique identifier — the engine surfaces it to FEEL on both throw and catch sides. Same instance ⇒ same id ⇒ correlation matches. Other instances of the same process see different ids and naturally don't correlate.

Cross-instance (application-level key)

To bridge two instances by some domain identifier, evaluate against your own variables:

<bpmn:message id="Msg_PaymentReceived" name="PaymentReceived">
<bpmn:extensionElements>
<zeebe:subscription correlationKey="=orderId" />
</bpmn:extensionElements>
</bpmn:message>

A catch in an order-processing instance evaluates =orderId against its own scope and registers under, say, 42. An external publish of PaymentReceived with correlation 42 then wakes that specific instance.

TTL

When a message is published with no waiting subscriber, the engine buffers it. The next subscriber to register that matches the name and correlation gets the buffered message and the buffer entry is consumed.

Each buffered message carries an expiry. If nobody correlates within the TTL, the entry is dropped.

SourceNotes
Per-publish via APIPass a ttl field with the publish request. Three formats accepted: ISO 8601 duration (PT1H, P1DT12H), Go-style duration (1h30m), or RFC 3339 absolute timestamp (2026-12-31T23:59:59Z)
Per-message in the modelSet <quantum:ttl> inside the <bpmn:message> definition. Workflow-internal publishers (intermediate throw, send task, message end event) use this
Server defaultUsed when neither caller nor model specifies one. 1 hour for messages

Publishing from outside the engine

Anything that can speak HTTP can publish a message:

POST /projects/{projectID}/bpmn/messages
FieldRequiredNotes
messageNameyesThe message name catches subscribed under
correlationKeysconditionalA primitive (string / number / boolean) or an object. Required to deliver to in-flight catch / boundary / ESP / receive listeners — must match the value the message def's correlationKey FEEL expression produces. Omit it (or send an empty object) only when triggering process-root message start events, which are matched by name alone and create new instances.
variablesnoVariable map merged into the receiving instance's scope
ttlnoBuffer expiry, formats as above. Defaults to 1 hour
curl -X POST "$API/projects/$PROJECT/bpmn/messages" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"messageName": "PaymentReceived",
"correlationKeys": 42,
"variables": { "amount": 49.95, "currency": "USD" },
"ttl": "PT1H"
}'

The same operation is available from the run panel on the instance detail page.


Signals

A signal is broadcast to every subscriber that matches by name. There's no correlation — the only filter is the signal name.

<bpmn:signal id="Sig_Abort" name="AbortRequested" />
<bpmn:intermediateThrowEvent id="throw-abort">
<bpmn:signalEventDefinition signalRef="Sig_Abort" />
</bpmn:intermediateThrowEvent>

<!-- in some other process: a non-interrupting boundary that listens for the abort -->
<bpmn:boundaryEvent id="abort-boundary" attachedToRef="long-task" cancelActivity="true">
<bpmn:signalEventDefinition signalRef="Sig_Abort" />
</bpmn:boundaryEvent>

Every running process that catches Sig_Abort receives it.

Buffer and TTL

Signals have a buffer too, but the semantics differ from messages:

  • A buffered signal is not consumed when a subscriber reads it. Late subscribers can still pick up a recent signal as long as the buffer entry is alive.
  • The default TTL for signal buffer entries is 24 hours, deliberately longer than the message default.
  • TTL accepts the same three formats as messages: ISO 8601 duration, Go duration, or RFC 3339 timestamp.
  • Per-publish (API), per-signal (<quantum:ttl> in the model), and the server default work the same way.

Publishing from outside the engine

POST /projects/{projectID}/bpmn/signals
FieldRequiredNotes
signalNameyesThe signal name
variablesnoVariable map merged into each receiving instance
ttlnoBuffer expiry. Defaults to 24 hours
curl -X POST "$API/projects/$PROJECT/bpmn/signals" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"signalName": "AbortRequested",
"variables": { "reason": "operator-cancelled" }
}'

The same operation is available from the run panel.


Choosing between them

A few patterns to keep in mind:

You want to…Use
Wake one specific instance by some application keyMessage with cross-instance correlation (correlationKey="=orderId" etc.)
Coordinate two parts of the same processMessage with self-correlation (correlationKey="=quantum.workflowId")
Tell every running instance "stop now"Signal
Notify all instances of a config changeSignal
Receive a callback from an external system about a specific requestMessage with correlation on the request ID
Trigger a new process instance from an external systemMessage start event (correlation optional — matched by name)

Both are point-to-name — there's no per-tenant or per-project isolation beyond the project boundary itself. Names are matched as-is, so prefix conventions (OrderProcess.PaymentReceived) help avoid collisions.