> ## 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.

# Quickstart

> Build and run a two-node Cerulion graph from scratch, then watch its messages live with topic echo.

<Frame>
  <img src="https://mintcdn.com/cerulion-5327fd47/lTRLJjqSsKCVFhMl/images/quickstart-hero.webp?fit=max&auto=format&n=lTRLJjqSsKCVFhMl&q=85&s=b47124b4bdde37188eb048567444a9b5" alt="The Cerulion lion wiring a camera input to a monitor output, building a two-node pipeline" width="1200" height="675" data-path="images/quickstart-hero.webp" />
</Frame>

## What you'll build

In this tutorial you build a complete two-node graph:

* A **periodic publisher** (`sensor`) that emits a `sensor_msgs/Image` on a
  fixed interval.
* A **data-triggered consumer** (`detector`) that fires each time a new image
  arrives and publishes a `geometry_msgs/PoseArray` of detections.

You will wire them into a graph, run it, and watch the messages flow with
`cerulion topic echo` in a second terminal.

```mermaid theme={null}
flowchart LR
  sensor[sensor] -- "sensor_msgs/Image" --> detector[detector]
  detector -- "geometry_msgs/PoseArray" --> out([detections])
```

*Figure: The two-node graph you build. The periodic `sensor` node publishes a `sensor_msgs/Image`; the data-triggered `detector` fires on each image and publishes a `geometry_msgs/PoseArray`.*

<Info>
  This guide assumes the `cerulion` CLI is installed and `cargo` is on your
  `PATH`. If not, follow [Installation](/cerulion/installation) first.
</Info>

## Build it step by step

<Steps>
  <Step title="Create and enter a workspace" icon="folder-plus">
    A workspace is the project container that holds your nodes, graphs, and
    schemas.

    ```bash theme={null}
    cerulion workspace create my_robot && cd my_robot
    ```

    Expected output:

    ```text theme={null}
    Created workspace at my_robot
    ```
  </Step>

  <Step title="Create the publisher node" icon="satellite-dish">
    Create a `sensor` node with one output port named `image` carrying a
    `sensor_msgs/Image`. Because it has no inputs, a source-only node must
    declare a non-data trigger policy; here, a 33 ms period.

    ```bash theme={null}
    cerulion node create sensor -o sensor_msgs/Image image --policy period_ms=33
    ```

    Expected output:

    ```text theme={null}
    Created node type 'sensor'
    ```

    <Info>
      The period is set with `--policy period_ms=33`. Note the underscore and
      the `=`.
    </Info>
  </Step>

  <Step title="Create the consumer node" icon="magnifying-glass">
    Create a `detector` node whose `image` input is a **trigger** (`-T`), so the
    node fires whenever an image arrives. Give it an output named `detections`
    carrying a `geometry_msgs/PoseArray`.

    ```bash theme={null}
    cerulion node create detector -T sensor_msgs/Image image -o geometry_msgs/PoseArray detections
    ```

    Expected output:

    ```text theme={null}
    Created node type 'detector'
    ```

    <Info>
      `-T` and `-o` each take **two** values in the order `SCHEMA NAME`. The
      trigger input also sets the node's policy to fire on that input's data.
    </Info>
  </Step>

  <Step title="Fill in the publisher's tick" icon="pen">
    Open `nodes/sensor/src/lib.rs` and replace its contents with the node below.
    Each tick stamps the image dimensions and bumps a frame counter.

    ```rust theme={null}
    use cerulion_core::prelude::*;
    use native_ros2_messages::sensor_msgs::Image;

    #[cerulion_node(period_ms = 33)]
    #[derive(Default)]
    struct SensorNode {
      #[output(data, encoding)]
      image: Image,

      frame_count: u32,
    }

    #[cerulion_node_impl]
    impl SensorNode {
      fn tick(&mut self) -> Result<(), NodeError> {
        self.frame_count += 1;
        self.image.height = 480;
        self.image.width = 640;
        self.image.encoding = "rgb8";
        Ok(())
      }
    }
    ```

    <Info>
      `height` and `width` are fixed fields, written straight to shared memory.
      `encoding` is a variable-length field, so it is listed in
      `#[output(data, encoding)]` — once listed, plain assignment works for it
      too.
    </Info>
  </Step>

  <Step title="Fill in the consumer's tick" icon="pen">
    Open `nodes/detector/src/lib.rs` and replace its contents with the node
    below. Each tick reads the latest image dimensions and counts a detection.

    ```rust theme={null}
    use cerulion_core::prelude::*;
    use native_ros2_messages::sensor_msgs::Image;
    use native_ros2_messages::geometry_msgs::PoseArray;

    #[cerulion_node]
    #[derive(Default)]
    struct DetectorNode {
      #[input(trigger)]
      image: Image,

      #[output]
      detections: PoseArray,

      detections_seen: u32,
    }

    #[cerulion_node_impl]
    impl DetectorNode {
      fn tick(&mut self) -> Result<(), NodeError> {
        let pixels = self.image.height * self.image.width;
        if pixels > 0 {
          self.detections_seen += 1;
        }
        Ok(())
      }
    }
    ```

    <Info>
      The `#[input(trigger)]` attribute on `image` is what makes `detector` fire
      on each incoming image. The node-level macro has no policy attribute
      because the trigger comes from the field.
    </Info>
  </Step>

  <Step title="Build both node crates" icon="hammer">
    Compile each node into a loadable library. These commands shell out to
    `cargo`.

    ```bash theme={null}
    cerulion node build sensor
    cerulion node build detector
    ```

    Expected output:

    ```text theme={null}
    Built 'sensor'
    Built 'detector'
    ```
  </Step>

  <Step title="Create a graph" icon="diagram-project">
    Create an empty graph named `perception` with an explicit topic prefix of
    `perception`. The `-n`/`--prefix` flag fixes the prefix so the topic names
    are predictable; without it, the prefix would default to your machine's
    hostname.

    ```bash theme={null}
    cerulion graph create perception -n perception
    ```

    Expected output:

    ```text theme={null}
    Created graph 'perception'
    ```

    <Info>
      Cerulion composes each topic name as `{prefix}/{node_id}/{output_name}`.
      With the prefix `perception`, the `sensor` instance's `image` output
      publishes to `perception/sensor/image`.
    </Info>
  </Step>

  <Step title="Stage the nodes and wire them" icon="link">
    Add an instance of each node to the graph. For `detector`, wire its `image`
    input to the `sensor` instance's `image` output with `-I`.

    ```bash theme={null}
    cerulion node stage sensor -g perception
    cerulion node stage detector -g perception -I image [sensor,image]
    ```

    Expected output:

    ```text theme={null}
    Staged 'sensor' into graph 'perception'
    Staged 'detector' into graph 'perception'
    ```

    <Info>
      `-I` takes the input port name followed by its source. The `[sensor,image]`
      form means "the `image` output of the node instance `sensor`."
    </Info>
  </Step>

  <Step title="Run the graph" icon="play">
    Run the graph. The default is live mode — the graph wakes and processes
    messages as they arrive. The run continues until you stop it with `Ctrl+C`.

    ```bash theme={null}
    cerulion graph run perception
    ```

    Leave this terminal running.
  </Step>

  <Step title="Watch the topics in a second terminal" icon="eye">
    Open a new terminal. Topic discovery needs no workspace. List the active
    topics, then echo the one carrying images:

    ```bash theme={null}
    cerulion topic list
    ```

    ```bash theme={null}
    cerulion topic echo perception/sensor/image
    ```

    `topic echo` pretty-prints `sensor_msgs/Image`, so you will see the image
    dimensions and encoding update as frames arrive. Stop echoing with `Ctrl+C`.

    <Info>
      Because you set the prefix to `perception`, the topic is
      `perception/sensor/image`. Always run `cerulion topic list` first to
      confirm the exact names before echoing.
    </Info>
  </Step>
</Steps>

<Check>
  You should see image messages streaming in the echo terminal while the graph
  runs. Your two-node graph is live: `sensor` publishes images on a 33 ms period
  and `detector` fires on each one. Press `Ctrl+C` in the run terminal to stop
  the graph.
</Check>

<Frame>
  <img src="https://mintcdn.com/cerulion-5327fd47/lTRLJjqSsKCVFhMl/images/thumbs-first.webp?fit=max&auto=format&n=lTRLJjqSsKCVFhMl&q=85&s=a0cc9d90c8360a0d173c8c5062c0fdf2" alt="The Cerulion lion on a podium with a gold medal celebrating a first successful graph run" width="1200" height="900" loading="lazy" decoding="async" data-path="images/thumbs-first.webp" />
</Frame>

## Next steps

<CardGroup cols={3}>
  <Card title="Concepts" icon="lightbulb" href="/cerulion/concepts" color="#0080FF">
    The mental model behind workspaces, nodes, graphs, topics, and schemas.
  </Card>

  <Card title="Guides" icon="book" href="/cerulion/guides/define-a-node" color="#0080FF">
    Task-focused walkthroughs for defining nodes and wiring graphs.
  </Card>

  <Card title="CLI reference" icon="terminal" href="/cerulion/reference/cli" color="#0080FF">
    Every command and flag, with synopses and examples.
  </Card>
</CardGroup>
