Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.cerulion.com/llms.txt

Use this file to discover all available pages before exploring further.

ROS2 relies on OS thread scheduling. Cerulion controls execution order explicitly. If your middleware can’t guarantee when nodes run and in what order, replaying a log won’t reproduce the same behavior. Debugging production failures becomes guesswork. Cerulion’s scheduler evaluates explicit triggers based on graph topology and input data — not OS threads, not callback queues, not timing jitter. Same inputs. Same execution order. Every time.

Trigger Model

Every node in Cerulion has an explicit trigger that determines when it runs. No implicit callback queues. No racing threads. The scheduler evaluates triggers deterministically — given the same inputs, nodes always execute in the same order.

Trigger Types

Periodic triggers

Nodes run on a fixed interval. Ideal for sensors and data sources that produce at a known rate.
#[node(period_ms = 33)]  // 30 Hz — fires every 33 ms
pub fn camera() -> (#[out("frame")] Image) {
    // The scheduler guarantees the period is respected
    // If the node overruns, the scheduler logs it — no silent skip
    (capture_frame())
}
The scheduler guarantees the period is respected regardless of system load. If a node overruns its period, the scheduler logs the overrun rather than silently skipping or double-firing.
Nodes run when a specific input receives new data. The trigger names the input explicitly — no ambiguity about which message caused execution.
#[node(trigger = "frame")]  // Fires when "frame" input has new data
pub fn detector(frame: &Image) -> (#[out("detections")] DetectionList) {
    // Explicit trigger — you know exactly what caused this node to run
    (run_detection(frame))
}
When multiple inputs have data, the scheduler processes triggers in a deterministic order based on graph topology — not arrival time.
Nodes wait for multiple inputs to arrive within a time window. Essential for sensor fusion where timing alignment matters.
#[node(sync = {"left": 50, "right": 50})]  // Both inputs within 50 ms
pub fn stereo(left: &Image, right: &Image) -> (#[out("depth")] DepthMap) {
    // The scheduler guarantees left and right are time-aligned
    // If inputs don't align within the window, the scheduler waits
    (compute_stereo(left, right))
}
The time windows (in milliseconds) define how close inputs must arrive to be considered synchronized. If inputs don’t align within the window, the scheduler waits rather than processing mismatched data.
Nodes run when a custom function returns true. For hardware interrupts, button presses, or any event outside the data flow.
#[node(external_trigger = "should_capture")]  // Custom trigger condition
pub fn triggered_capture() -> (#[out("image")] Image) {
    // Fires on hardware interrupt, button press, or custom logic
    (capture_high_res())
}

Why This Matters

The scheduler’s explicit trigger model means execution order is a function of the graph topology and input data — not of OS thread scheduling, system load, or timing jitter. Given the same graph and the same input sequence:
  • The same nodes fire in the same order
  • Synchronized triggers align on the same input pairs
  • Periodic nodes maintain their phase relationships
This is what makes deterministic resim possible. The scheduler is the foundation — zero-copy logging preserves the inputs, and the deterministic scheduler reproduces the execution.
For the complete API reference on trigger patterns, including code examples for each trigger type, see Publisher & Subscriber.
Ready to try Cerulion? Start with the quickstart and have a working camera-to-display pipeline in 5 minutes — or schedule a 15-min call with the team.