Core concepts
Events
Publish something that happened once, and let every interested task react to it. Events decouple the code that knows about a change from the work it triggers.
On this page
Publish and subscribe
A job says do this; an event says this happened. When your checkout code publishes order.paid, it doesn't need to know that a receipt is emailed, revenue charts update and the CRM syncs. Each of those is a subscription that turns the event into a job on a queue.
Because every delivery becomes an ordinary job, it gets the same retries, timeouts, priorities and logs as any other work. There is no second system to monitor.
Publishing an event
An event has a type in noun.verb form and a data object up to 64 KB. The API answers once the event is stored and its deliveries are queued, and returns how many subscriptions matched.
curl https://api.halyard.example/v3/events \ -H "Authorization: Bearer hy_test_51Hq8Zx2rTmV9cKpL3d" \ -H "Content-Type: application/json" \ -d '{ "type": "order.paid", "data": { "order_id": "ord_6Tn2Rk", "amount": 4900, "currency": "eur", "plan": "pro" }, "ordering_key": "cus_4Hd8Wk2Pq" }'const event = await halyard.events.publish({ type: "order.paid", data: { order_id: "ord_6Tn2Rk", amount: 4900, currency: "eur", plan: "pro" }, ordering_key: "cus_4Hd8Wk2Pq",}); console.log(event.id, event.deliveries); // evt_9Dk3Vr8Ms1 3event = halyard.events.publish( type="order.paid", data={"order_id": "ord_6Tn2Rk", "amount": 4900, "currency": "eur", "plan": "pro"}, ordering_key="cus_4Hd8Wk2Pq",) print(event.id, event.deliveries) # evt_9Dk3Vr8Ms1 3event, err := client.Events.Publish(ctx, &halyard.EventParams{ Type: "order.paid", Data: map[string]any{"order_id": "ord_6Tn2Rk", "amount": 4900, "currency": "eur", "plan": "pro"}, OrderingKey: "cus_4Hd8Wk2Pq",})event = halyard.events.publish( type: "order.paid", data: { order_id: "ord_6Tn2Rk", amount: 4900, currency: "eur", plan: "pro" }, ordering_key: "cus_4Hd8Wk2Pq"){ "id": "evt_9Dk3Vr8Ms1", "object": "event", "type": "order.paid", "data": { "order_id": "ord_6Tn2Rk", "amount": 4900, "currency": "eur", "plan": "pro" }, "ordering_key": "cus_4Hd8Wk2Pq", "published_at": "2026-10-14T10:41:22.507Z", "source": "api", "deliveries": 3}Subscribing
A subscription maps an event type to a queue and task. Declare subscriptions next to your queues in halyard.yaml so they are reviewed and deployed with your code:
# halyard.yamlsubscriptions: - event: order.paid queue: emails task: send-receipt - event: order.paid queue: reports task: update-revenue-dashboard - event: order.* queue: webhooks task: sync-crm-contact filter: data.plan in ["pro", "scale"]Or create them at runtime with a key that has the events:subscribe scope:
const sub = await halyard.subscriptions.create({ event: "order.paid", queue: "emails", task: "send-receipt",}); console.log(sub.id); // sub_4Pz7Kd1Xr5sub = halyard.subscriptions.create( event="order.paid", queue="emails", task="send-receipt",) print(sub.id) # sub_4Pz7Kd1Xr5The task receives the full event envelope as job.payload.event:
worker.task("send-receipt", async (job) => { const { event } = job.payload; // the full event envelope if (await receipts.exists(event.data.order_id)) return; // already sent await sendEmail({ template: "receipt", order: event.data.order_id }); await receipts.record(event.data.order_id, event.id);});Event types
You can publish any type you like. Halyard also publishes its own events about jobs and schedules, which you subscribe to the same way, for example to alert on failures.
| Type | Published by | Data fields |
|---|---|---|
| user.signed_up | Your app | user_id, email, plan |
| order.paid | Your app | order_id, amount, currency |
| invoice.finalized | Your app | invoice_id, customer_id |
| job.succeeded | Halyard | job_id, queue, task, duration_ms |
| job.failed | Halyard | job_id, queue, task, error, attempts |
| schedule.missed | Halyard | schedule_id, due_at, policy |
Patterns match on segments: order.* matches order.paid and order.refunded, job.failed matches only itself, and * matches everything you publish.
Fan-out
One event creates one job per matching subscription, on that subscription's queue. The jobs are independent: if the CRM sync fails and retries, the receipt still goes out on time.
- send-receiptqueue: emails
- update-revenue-dashboardqueue: reports
- sync-crm-contactqueue: webhooks
Filtering subscriptions
Add a filter to receive only the events you care about. Filters run before a job is created, so filtered-out events cost nothing and never show up in your queues.
| Filter | Matches |
|---|---|
| data.plan == "pro" | Exact match on a field |
| data.amount >= 10000 | Numeric comparison (amounts in minor units) |
| data.currency in ["eur", "gbp"] | Value in a list |
| exists(data.coupon) | Field is present and not null |
| data.country != "US" && data.amount > 0 | Combine with && and || |
halyard events test order.paid --file sample.jsonDelivery guarantees
Delivery is at-least-once. Every matching subscription gets a job, and that job retries until it succeeds or runs out of attempts. In rare cases, such as a worker crashing after it finished but before it reported back, a handler runs twice for the same event.
- Use
event.idas a natural deduplication key, as the handler above does. - Prefer upserts and "set to" updates over increments.
- Events are kept for 30 days and can be replayed to one subscription from the dashboard or API.
Ordering
By default events are delivered as fast as possible with no ordering between them. When order matters, set an ordering_key: events with the same key are delivered to each subscription one at a time, in publish order. A failing delivery holds back later ones with the same key until it succeeds or fails for good.