Connect two orbitals with an external event and a qualified listen, without either reaching into the other.
Orbitals never call each other or share state machines. One emits an event; another listens for it. This tutorial connects an order to a notification board.
An event that leaves its orbital is external, and must declare its payload:
1emits {
2 ORDER_SHIPPED -> external {
3 orderId : string!
4 }
5}SHIP -> shipped
(set @entity.status shipped)
(emit ORDER_SHIPPED { orderId: "A-1042" })Inside its own orbital a trait writes Trait.EVENT. Across orbitals, the source is qualified with the orbital's name:
1listens {
2 OrderOrbital.OrderLifecycle.ORDER_SHIPPED -> SHOW
3 SHOW {
4 orderId : string!
5 }
6}A two-part source naming a trait that only exists in another orbital is an error, and the message names the qualified form to use.
The program above as state machines, one per trait: each box is a state, each arrow an event that moves it, and the icons on an arrow are the effects that event runs. A ◇ on an arrow is a guard; hover it to read the check. How to read AVL
OrderLifecycle
on Order · states: 2 · transitions: 2NoticeBoard
on Notice · states: 1 · transitions: 1How the traits talk to each other, in order:
1app std-orb-docex-relay "1.0.0"
2"std-orb-docex-relay — two orbitals: one emits an external event, the other listens with the qualified three-part source."
3orbital OrderOrbital {
4 entity Order [runtime] {
5 id : string!
6 status : "pending" | "shipped" = pending
7 }
8
9 trait OrderLifecycle -> Order [interaction, instance] {
10 initial: pending
11
12 state pending {
13 INIT -> pending
14 (render-ui main { type: stack, direction: horizontal, gap: md, align: center, children: [
15 { type: badge, label: "Pending", variant: warning },
16 { type: button, label: "Ship order", action: SHIP, variant: primary }
17 ] })
18 SHIP -> shipped
19 (set @entity.status shipped)
20 (emit ORDER_SHIPPED { orderId: "A-1042" })
21 (render-ui main { type: stack, direction: horizontal, gap: md, align: center, children: [
22 { type: badge, label: "Shipped", variant: success },
23 { type: button, label: "New order", action: RESET, variant: ghost },
24 { type: button, label: "See notifications", href: "/notices", variant: link }
25 ] })
26 }
27
28 state shipped {
29 RESET -> pending
30 (set @entity.status pending)
31 (render-ui main { type: stack, direction: horizontal, gap: md, align: center, children: [
32 { type: badge, label: "Pending", variant: warning },
33 { type: button, label: "Ship order", action: SHIP, variant: primary }
34 ] })
35 }
36
37 emits {
38 ORDER_SHIPPED -> external {
39 orderId : string!
40 }
41 @description "An order left the warehouse."
42 }
43 }
44
45 page "/orders" -> OrderLifecycle
46}
47
48orbital NotificationOrbital {
49 entity Notice [runtime] {
50 id : string!
51 message : string = ""
52 }
53
54 trait NoticeBoard -> Notice [interaction, instance] {
55 initial: idle
56
57 state idle {
58 INIT -> idle
59 (render-ui main { type: stack, direction: vertical, gap: sm, children: [
60 { type: typography, content: "No notifications yet.", variant: body, color: muted },
61 { type: button, label: "Back to orders", href: "/orders", variant: link }
62 ] })
63 SHOW -> idle
64 (set @entity.message (str/concat "Order " ?orderId " shipped"))
65 (render-ui main { type: alert, variant: info, message: @entity.message })
66 }
67
68 listens {
69 OrderOrbital.OrderLifecycle.ORDER_SHIPPED -> SHOW
70 SHOW {
71 orderId : string!
72 }
73 }
74 }
75
76 page "/notices" -> NoticeBoard
77}The validator checks the wiring end to end: the event is declared, its payload matches what the listener reads, and an external event with no listener anywhere is reported.
A behavior that needs to be told which event to raise takes an event-typed knob, addressed with the same forms as listens: EVENT, Trait.EVENT or Orbital.Trait.EVENT. The address must name a trait in the program and one of its declared inputs.
Build a full app puts several orbitals and the standard library together.
The language where the rule is the program.