Working with events
The event bus is the platform’s connective tissue: mutations announce themselves, and the audit log, automation engine, notifications, and WebSocket broadcasts all react independently. This page is the developer’s contract; the full vocabulary is in the event catalogue.
Emitting
Section titled “Emitting”await event_bus.emit("challenge.solved", { "competition_id": competition_id, "challenge_id": challenge.id, "user_id": user.id, "team_id": team_id, "points": awarded, "is_first_blood": first_blood,})Rules:
- The name must exist in
backend/utils/event_catalog.py(EVENT_TYPES) — emitting an uncatalogued event is a bug, and the automation engine validates trigger names against the same list. - Names are
<entity>.<verb>, past tense: the event records something that happened, not a command. - Include
competition_idon competition-scoped payloads (the audit log and per-competition module gating rely on it), plus the IDs a consumer would need to act — think of the payload as the automation engine’s input.
Subscribing
Section titled “Subscribing”@event_bus.on("challenge.solved", owner="my_module")async def on_solve(event_name: str, payload: dict) -> None: ...
@event_bus.on("challenge.*", owner="my_module") # prefix wildcard@event_bus.on("*", owner="my_module", background=True) # everythingownerties handlers to a module so a disabled module’s handlers stop firing cleanly.- Lanes (ADR-0012): default (foreground) handlers are awaited before the
emitting request completes — right for the audit log and WS broadcasts,
where losing an event would be a correctness bug.
background=Truehandlers are scheduled fire-and-forget — required for anything slow or external. A failing handler is logged and isolated either way; it never breaks the emitting request or its sibling handlers. - There is deliberately no durable outbox: background delivery is at-most-once across a crash. If you’re building something that needs at-least-once semantics, raise it in an issue first — it’s an additive layer the architecture left room for.
Adding a new event type — the checklist
Section titled “Adding a new event type — the checklist”- Add the name to
EVENT_TYPESinbackend/utils/event_catalog.py. - Map its trigger permission in
TRIGGER_PERMISSIONS(backend/utils/automation_catalog.py) — which permission lets a rule author observe this event? A drift test fails if you skip this. - Optionally list its payload fields in
TRIGGER_FIELDS— that’s what the rule builder suggests for conditions and{placeholders}. Omitting it costs suggestions, never capability. - Emit it at the mutation site.
- Update
ARCHITECTURE.md§3.2 — the doc and the code are kept in lockstep, deliberately.
That’s the whole cost — and in exchange the event is instantly a first-class automation trigger, audit-log entry, and notification source with zero per-feature wiring.
Consuming events as an integrator
Section titled “Consuming events as an integrator”If you’re integrating from outside the process, you don’t subscribe to the bus — you point a webhook automation rule at your endpoint. The rule’s trigger/condition machinery gives you filtered, structured event delivery without touching platform code.