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

# Define a node

> Create a node type with ports and a trigger policy, then write its tick() in Rust.

A node is a Rust crate with a `#[cerulion_node]` struct and a `tick()` method. This guide takes you from `cerulion node create` to a node that reads inputs, writes outputs, and is ready to stage into a graph.

For the full attribute and policy grammar, see the [node macro reference](/cerulion/reference/node-macro) and [trigger policies reference](/cerulion/reference/trigger-policies).

<Info>
  Run these commands from inside a workspace (a directory with a `[workspace]`
  `Cargo.toml` and a `graphs/` folder). Create one with `cerulion workspace create <name>`.
</Info>

## Create the node type

`cerulion node create <node_type>` scaffolds `nodes/<node_type>/` with a `Cargo.toml` (cdylib) and a `src/lib.rs` macro template. The type name becomes the folder name and, PascalCased, the struct name. It must be non-empty, alphanumeric or underscore, and must not start with a digit.

Add ports while creating the node:

| Flag                     | Value names   | Meaning                                                 |
| ------------------------ | ------------- | ------------------------------------------------------- |
| `-o` / `--output`        | `SCHEMA NAME` | Add one output port.                                    |
| `-i` / `--input`         | `SCHEMA NAME` | Add one regular input port.                             |
| `-T` / `--trigger-input` | `SCHEMA NAME` | Add one trigger input (fires the node on data arrival). |
| `--policy`               | `SPEC`        | Set the trigger policy (see below).                     |

<Warning>
  Each of `-o`, `-i`, and `-T` takes **two** values (a schema and a name), and
  you may pass **at most one** of each per `create` call. Add more ports later
  with `node modify`. Schemas accept both `sensor_msgs/Image` and
  `sensor_msgs::Image`; both canonicalize to the slash form.
</Warning>

### Pick a trigger policy

The trigger policy decides *when* the node fires. Pass it with `--policy SPEC`:

| `--policy` SPEC     | Fires when…                                              |
| ------------------- | -------------------------------------------------------- |
| `period_ms=N`       | Every N milliseconds (N > 0).                            |
| `sync_window_ms=N`  | All trigger inputs have a message within an N-ms window. |
| `external`          | The host triggers it explicitly.                         |
| `data_trigger=NAME` | The named trigger input receives data.                   |

<Frame>
  <img src="https://mintcdn.com/cerulion-5327fd47/lTRLJjqSsKCVFhMl/images/zero-copy-timestamps.webp?fit=max&auto=format&n=lTRLJjqSsKCVFhMl&q=85&s=7c824851ab9207c128f0454f72c7e951" alt="The Cerulion lion pointing at a giant stopwatch — trigger policies decide exactly when a node fires" width="1200" height="900" loading="lazy" decoding="async" data-path="images/zero-copy-timestamps.webp" />
</Frame>

How the policy defaults when you omit `--policy` depends on the node's inputs:

* **0 inputs (no `-i`, no `-T`):** a source-only node **must** declare a non-data policy. `cerulion node create` errors otherwise.
* **1+ inputs via `-i` only:** no node-level policy is written; the runtime fires on any input arrival and emits a graph-build warning.
* **`-T` set:** the policy becomes `data_trigger` for that trigger input.

<Warning>
  A source-only node (zero inputs) has nothing to fire it, so you must give it an
  explicit non-data policy with `--policy period_ms=N` or `--policy external`.
  Combining a non-data `--policy` with `-T` is a conflict and errors.
</Warning>

<Steps>
  <Step title="Create a periodic source node" icon="camera">
    A camera with no inputs needs a period:

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

    ```text theme={null}
    Created node type 'camera'
    ```
  </Step>

  <Step title="Create a data-triggered consumer" icon="magnifying-glass">
    A detector that fires whenever an image arrives:

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

    ```text theme={null}
    Created node type 'detector'
    ```
  </Step>
</Steps>

## Write the tick()

Open `nodes/<node_type>/src/lib.rs`. The template pairs two macros:

* `#[cerulion_node(...)]` on the struct declares the node and its trigger policy, and generates the glue that lets `cerulion graph run` load it.
* `#[cerulion_node_impl]` on the adjacent `impl` block lets `tick()` use plain field access for ports — reads and writes go directly to shared memory, zero-copy. It takes no arguments, and the struct must appear before the impl block.

Import everything from the prelude, and import the message types you use:

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

#[cerulion_node(period_ms = 10)]      // period-driven; reads the latest scan each tick
#[derive(Default)]
struct SafetyController {
    #[input(lifo, depth = 1)]         // regular input (use `trigger` for data-triggered)
    scan: LaserScan,

    #[output]                         // Vector3 = fixed-only schema (pub x/y/z: f64)
    linear_velocity: Vector3,
}

#[cerulion_node_impl]
impl SafetyController {
    fn tick(&mut self) -> Result<(), NodeError> {
        // Variable field read via typed accessor:
        let obstacle_close = self.scan.ranges().iter().any(|&r| r < 0.5);
        // Fixed field write; goes straight to shared memory:
        self.linear_velocity.x = if obstacle_close { 0.0 } else { 0.3 };
        Ok(())
    }
}
```

### Fixed vs variable fields

How you write an output field depends on whether it is fixed-size or variable-length:

* **Fixed primitive fields** (for example `x`, `y`, `z`, `height`, `width`) are written directly: `self.linear_velocity.x = 0.3;`. The write goes straight to shared memory.
* **Variable-length fields** (`string`, `T[]`, nested types) must be listed in the `#[output(...)]` attribute; you then write them with plain assignment too.

List simple variable fields by name in `#[output(...)]`:

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

#[cerulion_node(period_ms = 33)]
#[derive(Default)]
struct Camera {
    #[output(data, encoding)]   // `data` and `encoding` are variable fields
    image: Image,
}

#[cerulion_node_impl]
impl Camera {
    fn tick(&mut self) -> Result<(), NodeError> {
        self.image.height = 480;          // fixed field, direct write
        self.image.width = 640;           // fixed field, direct write
        self.image.encoding = "rgb8";     // variable field — plain assignment, listed in #[output]
        Ok(())
    }
}
```

<Note>
  For nested-typed variable fields, use the `complex(...)` form, for example
  `#[output(data, complex(header))]`. See the
  [node macro reference](/cerulion/reference/node-macro) for the full `#[output]` grammar.
</Note>

<Tip>
  **Zero-copy write for large payloads:** plain `=` assignment copies your buffer once. For
  camera drivers or codecs that can write directly into a destination buffer, use `fill_from`
  instead — your producer receives the destination buffer itself, skipping the copy:

  ```rust theme={null}
  self.image.data.fill_from(|buf: &mut [u8]| {
      let n = driver.read_frame_into(buf)?;
      Ok(n)
  })?;
  ```

  `FillFrom` and `SliceSource` are re-exported from `cerulion_core::prelude`. See the
  [node macro reference](/cerulion/reference/node-macro#fill_from-zero-copy-producer-writes) for details.
</Tip>

## Build the node

`cerulion node build <node_type>` compiles the crate into a cdylib. Add `--release` for an optimized build.

<Warning>
  `node build` shells out to `cargo`, so `cargo` must be on your `PATH`.
</Warning>

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

```text theme={null}
Built 'camera'
```

<Check>
  A successful build prints `Built '<node_type>'`. On failure the CLI prints
  `Error:` with the cargo output, and no cdylib is produced.
</Check>

## Inspect and adjust

Use these commands to review and edit node types after creation.

<AccordionGroup>
  <Accordion title="List node types" icon="list">
    `cerulion node list` prints a table of types with input/output counts and a short policy label.

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

    ```text theme={null}
    TYPE       INPUTS  OUTPUTS  POLICY
    camera     0       1        period 33ms
    detector   1       1        trigger:image
    ```
  </Accordion>

  <Accordion title="Show one node's details" icon="circle-info">
    `cerulion node info <node_type>` prints the type, policy, and each port with its schema. Metadata is parsed from `src/lib.rs`; there is no sidecar file.

    ```bash theme={null}
    cerulion node info detector
    ```
  </Accordion>

  <Accordion title="Add ports later" icon="pen">
    `cerulion node modify <node_type>` mutates `src/lib.rs` in place, preserving the tick body and comments. It takes the same `-i`, `-T`, `-o`, and `--policy` flags (at most one of each per call).

    ```bash theme={null}
    cerulion node modify detector -i sensor_msgs/Imu imu
    ```

    ```text theme={null}
    Added input 'imu' to 'detector'
    ```

    Adding a trigger input with `-T` also sets the `data_trigger` policy. To make a node externally triggered, use `--policy external`.
  </Accordion>

  <Accordion title="Delete a node type" icon="trash">
    `cerulion node delete <node_type>` removes the node crate and its workspace member entry.

    ```bash theme={null}
    cerulion node delete detector
    ```

    ```text theme={null}
    Deleted node type 'detector'
    ```
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Wire and run a graph" icon="diagram-project" href="/cerulion/guides/wire-and-run-a-graph" color="#0080FF">
    Stage these node types into a graph and run it.
  </Card>

  <Card title="Trigger policies" icon="bolt" href="/cerulion/reference/trigger-policies" color="#0080FF">
    The full `--policy` grammar and defaulting matrix.
  </Card>
</CardGroup>
