Your First Routing Rules
Routing in Beach is JSON: a list of rules saying "an event with this source and this eventType goes to that handler". A reader of a Beach repository's routing configuration can therefore see exactly how every event flows, without running the code.
This is the five-minute introduction. For the full predicate DSL and the cascade rules, see Creating routing rules.
The minimum
import { EventRouter } from '@cool-ai/beach-core';
const router = new EventRouter();
// 1. Register handlers by name
router.register('search', async (event) => {
console.log('searching for:', event.data);
});
router.register('default-reply', async (event) => {
console.log('default reply for:', event.data);
});
// 2. Load the routing config
router.loadRoutingConfig({
rules: [
{ source: 'user', eventType: 'message', handler: 'search',
when: { payload: { intent: { equals: 'search' } } } },
{ source: 'user', eventType: 'message', handler: 'default-reply' },
],
});
// 3. Dispatch events
await router.routeEvent({
source: 'user', eventType: 'message',
data: { intent: 'search', query: 'beach holidays' },
});
// → the search handler runs
await router.routeEvent({
source: 'user', eventType: 'message',
data: { text: 'hello' },
});
// → default-reply runs; the search rule's `when` did not match
Three concepts to remember:
sourceandeventType— the routing key. Every event carries both.handler— the registered handler's name. The handler must already be registered when an event arrives for it.when— an optional payload predicate. The first matching rule wins; ifwhendoes not match, dispatch falls through to the next rule that shares the same(source, eventType).
When no rule matches, routeEvent throws RouteNotFoundError. The router never silently drops an event.
The predicate DSL — quick reference
{ payload: { status: { equals: 'confirmed' } } } // exact match
{ payload: { region: { exists: true } } } // field present
{ payload: { region: { exists: false } } } // field absent
// Combinations
{ anyOf: [
{ payload: { departureDate: { exists: true } } },
{ payload: { travelMonth: { exists: true } } },
] }
{ allOf: [
{ payload: { destination: { exists: true } } },
{ payload: { passengers: { exists: true } } },
] }
payload, anyOf, and allOf can all appear in one clause; all three must hold for the rule to match. See Creating routing rules for the full DSL.
Three kinds of rule
| Kind | What it does |
|---|---|
| Routing | Dispatches an event to exactly one handler (1:1). |
| Filtering | Fans an event out to additional destinations after routing (1:N). |
| Cascade | Fires a derived event when the original matches a predicate (1:N). |
Routing is required. Filtering and cascade are optional layers built on top.
What not to do
Do not rely on rule ordering across different (source, eventType) pairs. The router has no global order; only the when-guarded rules that share the same routing key are compared in declaration order.
Do not route every event through 'concierge' "just in case". Decide which (source, eventType) pairs the application emits and which handler each one goes to. The routing configuration should make that decision legible to anyone who opens it.
Do not put logic in routing predicates that needs database access. The predicate DSL is structural — equals, exists. If a routing decision needs "look up the user's tier", that is an actor's job, or a handler's, not a predicate's. The handler can emit a follow-on event with the tier already attached, and the next rule can branch on the typed field.
Do not call loadRoutingConfig more than once. The call replaces the entire ruleset; load it at startup and leave it.
Do not dispatch through the router for in-process function calls. The router exists for cross-component communication. If handlerA always wants to call handlerB synchronously with the same data, just call the function. The router's audit and observability value lives in the cross-component visibility; abusing it for local control flow buries the signal.
A real-world shape
A small productivity-assistant routing configuration:
router.loadRoutingConfig({
rules: [
// Inbound — every email goes to triage first
{ source: 'email', eventType: 'received', handler: 'triage' },
// Triage's output routes by class
{ source: 'assistant', eventType: 'triage_complete',
handler: 'auto-archive',
when: { payload: { class: { equals: 'junk' } } } },
{ source: 'assistant', eventType: 'triage_complete',
handler: 'concierge',
when: { payload: { class: { equals: 'actionable' } } } },
// Concierge's reply
{ source: 'assistant', eventType: 'reply_ready', handler: 'reply-dispatcher' },
// Reply dispatch by channel
{ source: 'streaming', eventType: 'deliver', handler: 'chat-collector' },
{ source: 'batched', eventType: 'deliver', handler: 'response-collector' },
],
});
A reader of that configuration can predict where any event goes without opening a single handler.
Related
- Creating routing rules — full predicate DSL, cascade, filtering, retries.
- Your first handler — what runs when a routing rule fires.
- Using the starter scaffold — the canonical pipeline's routing config.