Exactly-once event subscriptions
Every replica sees every event, and a subscription still starts one workflow per event, through a deterministic id and idempotent start.
The situation
An order failure should open one triage run. Long Tail runs as four replicas, and each receives every published event. Without coordination the subscription would fire four times.
The recipe
agents: [{ name: 'order-triage', description: 'Opens a triage run for every failed order.', subscriptions: [{ topic: 'app.orders.failed', reaction_type: 'durable', workflow_type: 'triageOrder', input_mapping: { data: { orderId: '{event.data.orderId}', error: '{event.data.error}' } }, filter: { channel: 'web' }, }],}],Topics use NATS-style wildcards: * matches one segment, > matches the rest. filter is a shallow match on event.data. A reaction is a durable workflow, a compiled pipeline (pipeline) or a dynamic MCP query (mcp_query).
Each replica computes the same workflow id for the event:
agent-{agentId}-{subscriptionId[0:8]}-{eventKey}eventKey comes from the event’s originating workflow, task or escalation id, or a hash of timestamp, type and payload for custom events.
The guarantee
The first replica to start the workflow wins; the others get a duplicate-job error, which is caught and ignored. The result is exactly one workflow execution per event, whatever the replica count. Include a stable identifier in event.data when publishing custom events. Lifecycle events (agent.started, agent.completed) are observability side effects and may repeat. See Agents and events.
Related patterns
Agents and MCP
Goals, rules and memory
An agent automation carries goals that drive it, rules that constrain it and a knowledge domain it accumulates into over time.
Agents and MCP
Autonomous topic wiring
Topics carry payload schemas in a catalog, so an agent can discover what is published and wire a subscription entirely through tool calls.
Agents and MCP
An event is a notification
Escalation events carry routing fields and facets only; a subscriber that needs the payload reads the row by id through the scoped API.
