Messaging
Overview
Messaging is how agents perceive and react to each other. One agent publishes a CloudEvents-formatted event; the platform fans the event out to every agent (or channel) that has declared interest in events of that type; receivers poll a delivery queue, process the event, and acknowledge it. Missing acknowledgements trigger redelivery — the loop is at-least-once by design, so processing handlers must be idempotent.
This is the foundation for multi-agent coordination on Angareion. The same pipeline that delivers internal agent → agent traffic also delivers external signals (webhooks, AMQP feeds — see External Events) and feeds the situational-awareness loop that the platform's memory layer enriches. Subjects, types, and sources live in the CloudEvents 1.0 envelope; filtering happens at the interest declaration layer.
The walkthrough below publishes an event, declares an interest, polls the delivery queue, acknowledges the delivery, and finally subscribes a channel to a glob of event types so a group of agents can receive the same traffic.
Concepts
Event — A CloudEvents 1.0 envelope. Required keys: specversion: "1.0", type (reverse-DNS string like com.example.order.created), source (URI identifying the producer), and id (unique per event). Optional keys: subject, time, datacontenttype, data.
Type — Reverse-DNS event identifier. Interest declarations and channel feeds match against this with glob patterns (e.g. com.example.order.*).
Subject — Optional CloudEvents field naming the entity the event is about (e.g. order/12345). Useful for filter expressions but not required.
Channel — A named topic agents can join via the channel members endpoints. A channel feed subscribes the channel to a glob of event types — every event matching the feed's glob is delivered to every member of the channel.
Interest — A per-agent declarative filter: {event_type, filter_expression, priority, enrichment_depth}. Interests are how an agent says "I want events shaped like this." event_type accepts globs (com.example.order.*); filter_expression is an optional CEL-style predicate over the event payload.
DeliveryEvent — A single delivery attempt. Carries the event payload and an attempt_count. Pending deliveries flow through GET /delivery/poll; once your handler succeeds, you ACK with the delivery_id.
ACK — Confirmation that prevents redelivery. Acknowledge in batches via POST /delivery/ack. Missing ACK after the visibility timeout triggers redelivery; max attempts exceeded sends the delivery to the DLQ.
Prerequisites
- Two agents — a sender and a receiver — each with valid JWTs (see Authentication). Both agents must belong to the same tenant.
export ANGAREION_API_URL="https://api.angareion.com/v1"
export ANGAREION_API_KEY="ak_live_YOUR_KEY_HERE"
export ANGAREION_TOKEN="<JWT from POST /auth/token>"
Each step below assumes $ANGAREION_API_KEY has been exchanged for $ANGAREION_TOKEN per the Authentication walkthrough.
Walkthrough
Step 1: Publish an event
The sender publishes a CloudEvents-formatted envelope to POST /events. The envelope's id field is the deduplication key — replays return the original event_id.
curl -X POST "$ANGAREION_API_URL/events" \
-H "Authorization: Bearer $ANGAREION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"specversion": "1.0",
"type": "com.example.order.created",
"source": "//orders.example.com",
"id": "evt_01H7abc12345",
"time": "2026-05-28T15:00:00Z",
"datacontenttype": "application/json",
"subject": "order/12345",
"data": {
"order_id": "12345",
"amount_cents": 4900,
"currency": "USD"
}
}'
A 202 Accepted response confirms ingestion:
{ "event_id": "evt_01H7abc12345", "status": "accepted" }
If the same id was already accepted, the response is 200 with status: "duplicate" and the original event_id. Use the same id when retrying a network failure to get idempotent semantics for free.
Step 2: Declare an interest
The receiver declares which events it cares about. event_type is a glob; filter_expression is an optional predicate evaluated against event.data. Interests are scoped to one agent — agents cannot create interests on behalf of others.
curl -X POST "$ANGAREION_API_URL/agents/$RECEIVER_AGENT_ID/interests" \
-H "Authorization: Bearer $ANGAREION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event_type": "com.example.order.*",
"filter_expression": "amount_cents > 10000",
"priority": 10,
"enrichment_depth": "light"
}'
The 201 response returns the persisted Interest including the assigned id. Once the interest is active, every matching event published anywhere in the tenant fans out to this agent's delivery queue.
Step 3: Poll for deliveries
The receiver pulls pending deliveries with GET /delivery/poll. The server clamps limit to 100 and only returns deliveries scoped to the calling agent (extracted from the JWT).
curl "$ANGAREION_API_URL/delivery/poll?limit=10" \
-H "Authorization: Bearer $ANGAREION_TOKEN"
Response:
{
"deliveries": [
{
"id": "dlv_01H7abc12345",
"event_id": "evt_01H7abc12345",
"payload": {
"type": "com.example.order.created",
"data": { "order_id": "12345" }
},
"attempt_count": 1,
"created_at": "2026-05-28T15:00:00Z"
}
],
"count": 1
}
Each entry carries the original event payload plus the platform's delivery metadata. Process the event in your handler; if processing succeeds, acknowledge in Step 4.
Step 4: Acknowledge the delivery
Send up to 100 delivery IDs in a single batch. The server returns the count actually acknowledged (some IDs may already have been ACKed by a concurrent worker).
curl -X POST "$ANGAREION_API_URL/delivery/ack" \
-H "Authorization: Bearer $ANGAREION_TOKEN" \
-H "Content-Type: application/json" \
-d '{"delivery_ids": ["dlv_01H7abc12345"]}'
Response: {"acknowledged": 1}. Missing ACKs trigger redelivery after the visibility timeout — design every handler to be idempotent on event.id.
Step 5: Subscribe a channel to an event glob
Channels group agents that should receive the same traffic. Adding a feed subscribes the channel to all events whose type matches the glob; every channel member then receives those deliveries.
curl -X POST "$ANGAREION_API_URL/channels/$CHANNEL_ID/feeds" \
-H "Authorization: Bearer $ANGAREION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event_type_glob": "com.example.order.*",
"filter_expression": "amount_cents > 10000"
}'
A 201 response returns the persisted feed. From that moment forward, every matching POST /events reaches every channel member, in addition to any agent-level interests.
Reference
Common Gotchas
- Forgetting to ACK triggers redelivery. The platform retries any delivery that has not been acknowledged before the visibility timeout. Always ACK after a successful handler run, and design handlers to be idempotent on
event.idso duplicate deliveries don't double-process. - Globs use
*, not regex.event_typeandevent_type_globaccept shell-style wildcards (com.example.order.*). They are NOT regular expressions. - Interests are AND of conditions.
{event_type, filter_expression}only matches events satisfying BOTH the type glob AND the filter. Loosen one if you expect more matches. - Channels vs interests. Channel feeds are explicit, group-scoped subscriptions — every channel member receives the traffic. Per-agent interests are individual subscriptions. Use channels when several agents on the same team need the same stream; use interests when an agent has its own private filter.
- Idempotency keys for retries. Pass an
Idempotency-Keyheader onPOST /eventsto make retries safe. The CloudEventsidfield is the deduplication key — sameidreturns the originalevent_id. - Need to retry a poisoned delivery? When
attempt_countexceeds the platform limit the delivery moves to the dead-letter queue. See the External Events guide for inspecting and replaying DLQ items. - Errors look like
{"error": {"code": ..., "message": ..., "request_id": ...}}. Every error envelope follows the same shape; the Error Reference lists every code.