Skip to main content
When a handler emits another event, Bubus automatically records lineage so you can understand call chains instead of guessing what triggered what. Repository example files:

What gets tracked

  • event_parent_id: points from child -> parent event
  • event_children: aggregated list of children emitted by handler execution
  • event_emitted_by_handler_id: which specific handler emitted the child
This tracking works across nested chains (parent -> child -> grandchild) and is surfaced in event helpers and tree logs. Parent-child links are recorded when you emit from inside a running handler context:
  • Python: event.event_bus.emit(...)
  • TypeScript: event.bus?.emit(...)
Using the event-scoped bus keeps ancestry metadata intact automatically.

Works across forwarded buses too

Parent-child lineage is preserved even when the parent event has been forwarded between buses. If a forwarded event is handled on another bus and that handler emits a child:
  • the child still gets event_parent_id = <forwarded parent event_id>
  • the child is linked under the emitting handler’s event_children
  • forwarding that child onward keeps the same lineage metadata
Use the event-scoped bus in handlers (event.event_bus / event.bus) so the runtime can attach ancestry correctly. See also: Forwarding Between Buses

Queue-jumped vs normally queued children

Lineage tracking works in both execution styles:
  • Queue-jumped child events:
    • emitted inside a handler and immediately awaited (await child / await child.done())
    • child may execute right away (RPC-style), but still gets normal parent linkage metadata
  • Normally queued child events:
    • emitted inside a handler but not immediately awaited
    • child runs later via normal queue scheduling, and still keeps the same event_parent_id ancestry link
In short: queue-jump changes when the child executes, not whether parent-child tracking is recorded. See Immediate Execution (RPC-style) for queue-jump behavior details.

Full example: checkout -> reserve/charge/receipt (+ fraud grandchild)

from bubus import BaseEvent, EventBus

class CheckoutEvent(BaseEvent[str]):
    order_id: str

class ReserveInventoryEvent(BaseEvent[str]):
    order_id: str

class ChargeCardEvent(BaseEvent[str]):
    order_id: str

class FraudCheckEvent(BaseEvent[str]):
    order_id: str

class SendReceiptEvent(BaseEvent[str]):
    order_id: str

bus = EventBus('TreeBus')

async def on_checkout(event: CheckoutEvent) -> str:
    reserve = event.event_bus.emit(ReserveInventoryEvent(order_id=event.order_id))
    await reserve
    reserve_id = await reserve.event_result()

    charge = event.event_bus.emit(ChargeCardEvent(order_id=event.order_id))
    await charge
    charge_id = await charge.event_result()

    receipt = event.event_bus.emit(SendReceiptEvent(order_id=event.order_id))
    await receipt
    receipt_id = await receipt.event_result()

    return f'{reserve_id}|{charge_id}|{receipt_id}'

async def on_reserve(event: ReserveInventoryEvent) -> str:
    return f'reserve:{event.order_id}'

async def on_charge(event: ChargeCardEvent) -> str:
    fraud = event.event_bus.emit(FraudCheckEvent(order_id=event.order_id))
    await fraud
    fraud_status = await fraud.event_result()
    return f'charge:{event.order_id}:{fraud_status}'

async def on_fraud(event: FraudCheckEvent) -> str:
    return f'fraud-ok:{event.order_id}'

async def on_receipt(event: SendReceiptEvent) -> str:
    return f'receipt:{event.order_id}'

bus.on(CheckoutEvent, on_checkout)
bus.on(ReserveInventoryEvent, on_reserve)
bus.on(ChargeCardEvent, on_charge)
bus.on(FraudCheckEvent, on_fraud)
bus.on(SendReceiptEvent, on_receipt)

root = bus.emit(CheckoutEvent(order_id='ord-123'))
result = await root.event_result()
await bus.wait_until_idle()

print(result)
print(bus.log_tree())

Example tree output

Captured from running the Python example above with uv run (IDs/timestamps vary run-to-run):
└── CheckoutEvent#b7c7 [10:10:54.522 (0.003s)]
    └── ✅ TreeBus#ef2a.__main__.on_checkout#7a12 [10:10:54.522 (0.002s)] → 'reserve:ord-123|charge:ord-123:fraud-ok:ord-123|receipt:ord-123'
        ├── ReserveInventoryEvent#ca2f [10:10:54.522 (0.000s)]
        │   └── ✅ TreeBus#ef2a.__main__.on_reserve#1583 [10:10:54.522 (0.000s)] → 'reserve:ord-123'
        ├── ChargeCardEvent#b746 [10:10:54.523 (0.001s)]
        │   └── ✅ TreeBus#ef2a.__main__.on_charge#7d9c [10:10:54.523 (0.001s)] → 'charge:ord-123:fraud-ok:ord-123'
        │       └── FraudCheckEvent#31e0 [10:10:54.523 (0.000s)]
        │           └── ✅ TreeBus#ef2a.__main__.on_fraud#4c4e [10:10:54.523 (0.000s)] → 'fraud-ok:ord-123'
        └── SendReceiptEvent#c399 [10:10:54.524 (0.000s)]
            └── ✅ TreeBus#ef2a.__main__.on_receipt#de9f [10:10:54.524 (0.000s)] → 'receipt:ord-123'

Why this helps in practice

  • Debugging: quickly see causality chains instead of inspecting raw logs line-by-line.
  • Reliability: timeout/cancellation behavior can be reasoned about by ancestry.
  • Querying: combine lineage with find(..., child_of=...) to isolate event families.