---
title: "1. Start from the scaffold"
description: "Create the motion watcher project with tatolab new, run it, and find your way around its files."
type: "docs-page"
url: "https://tatolab.com/docs/tutorial/scaffold"
site: "Tatolab"
section: "Tutorial"
---

# 1. Start from the scaffold

> Create the motion watcher project with tatolab new, run it, and find your way around its files.

`tatolab new` writes a small, working stream. You'll change it into the motion watcher over the next steps.

## Create and run the project

```sh
tatolab new motion-watcher
cd motion-watcher
uv sync
tatolab dev
```

A window opens with your camera, colors inverted, and the terminal shows a `brightness` record every second. Keep `tatolab dev` running for the whole tutorial. Each time you save a file, it loads the stream again with your change.

If `tatolab dev` says the name `main` is held by another stream, the quickstart's stream is still loaded: press Ctrl-C in its terminal, or run `tatolab rm main` if you kept it.

## A tour of `stream.py`

Open `stream.py`. Apart from its docstring, it's this:

```python title="stream.py"
from tatolab.stream import CameraSource, DisplayWindow, StreamBuilder, stream

from nodes.brightness_meter import BrightnessMeter
from nodes.inverting_effect import InvertingEffect


@stream
def main(stream_builder: StreamBuilder) -> None:
    """Camera, inverted, in a window; brightness logged once a second."""
    source = stream_builder.add(CameraSource)
    effect = stream_builder.add(InvertingEffect)
    meter = stream_builder.add(BrightnessMeter)
    window = stream_builder.add(
        DisplayWindow, config={"title": "Tatolab", "scaling": "fit"}
    )
    stream_builder.connect(source.output("video"), effect.input("video_from_upstream"))
    stream_builder.connect(effect.output("video_to_downstream"), window.input("video"))
    stream_builder.connect(
        effect.output("video_to_downstream"), meter.input("video_from_upstream")
    )
    stream_builder.expose(effect.output("video_to_downstream"))
```

Line by line:

- `@stream` marks `main` as a stream. The runtime calls it once, when the stream loads, to find out which nodes to create and how to connect them. It doesn't run per frame.
- `stream_builder.add(CameraSource)` adds a node. You pass the class itself, not an instance. `CameraSource` and `DisplayWindow` are built into the runtime; `InvertingEffect` and `BrightnessMeter` are your project's own, in `nodes/`.
- `config={...}` sets a node's settings. For built-in nodes, your editor can complete the keys and flag typos.
- `stream_builder.connect(output, input)` links an output port to an input port. The effect's output is connected twice, to the window and to the meter: one output can feed any number of inputs, and it isn't split between them.
- `stream_builder.expose(...)` lets other streams on this machine read that output. Outputs are internal to their stream until you expose them. You'll use this in step 3.

## A tour of the nodes

`nodes/inverting_effect.py` is a **GPU effect**: one GLSL function that runs on the GPU for every pixel of every frame.

`nodes/brightness_meter.py` is a **CPU node**. Once a second it reads one frame's pixels as a numpy array with `frame.cpu()`, computes the average brightness, and logs it. It runs in its own process, reading the effect's output beside the window, so if it were slow, the window wouldn't notice.

In the next step you'll replace both with a node of your own.

## Rename the stream

The stream's name comes from the function's name, so this one is called `main`. Give it a name that says what it is. In `stream.py`, change the function's name, its docstring and the window's title:

```python title="stream.py" {2,3,8}
@stream
def motion_watcher(stream_builder: StreamBuilder) -> None:
    """Watches the camera for motion."""
    source = stream_builder.add(CameraSource)
    effect = stream_builder.add(InvertingEffect)
    meter = stream_builder.add(BrightnessMeter)
    window = stream_builder.add(
        DisplayWindow, config={"title": "Motion watcher", "scaling": "fit"}
    )
    # … the connect and expose lines stay as they are
```

Save. `tatolab dev` loads the stream again, now named `motion_watcher`, and the window's title changes. The name matters later: it's how you refer to the stream in commands like `tatolab logs --stream motion_watcher`, and it's part of every exposed port's address.

## Next

Next you'll write a node that reports motion: [Write your first node](https://tatolab.com/docs/tutorial/first-node).
