# The job graph

Jobs, nodes, edges, fingerprints, staleness, approvals, and the planner that grows a video's graph.

A job is a graph of nodes, run like a build system, that the planner grows as the video takes shape.

## Jobs

A job is one thing rapid makes: a video, an empty job for nodes added over the API (`rapid new`), or one command run through a lane (`rapid lane`). It has a title, a brief, a project folder, a style and format, an `error` when its plan failed, and a state:

| Job state | When |
|---|---|
| `running` | something is queued or running, or still waiting |
| `needs_you` | any node waits on you |
| `done` | every node is done |
| `failed` | a node failed and nothing else can move |
| `cancelled` | you cancelled it |

## Nodes

A node is one unit of work with its own output. Each has a type, a lane, a label, a group (the scene it belongs to), the nodes it needs, its outputs, its state, its attempt count, its timings, and a spec saying what to run.

| Type | What it does |
|---|---|
| `storyboard` | an agent writes `storyboard.json` |
| `scene` | an agent builds or revises `scenes/<id>.html`, or builds its key frame |
| `render` | renders a scene to MP4 with its boxes and cues |
| `check` | the code checks, a contact sheet, a micro strip, a key-frame still, or the cut's sheet (`rapid video-sheet`) |
| `review` | an agent reviews a key frame or a round into marks, or the director judges the storyboard or the video |
| `approval` | waits for you |
| `listen` | hears the video's track and writes its map (`rapid listen`) |
| `blender` | renders a shot's key stills, a chunk of its frames, or its clips (`rapid shot`) |
| `mix` | mixes the video's sound from the cues, over the track when there is one |
| `mux` | joins the scenes, or puts the mix under the picture |
| `phone` | makes the phone copy |
| `scan`, `extract`, `custom` | other work; `custom` is what `rapid lane` runs |

A node is either a **process** node, which runs a command in its lane, or an **agent** node, which runs a prompt as a Claude Agent SDK session in the project folder, with the plugin's agent types and skills, a limited set of tools, and no permission prompts.

| Node state | Means |
|---|---|
| `waiting` | some node it needs isn't done |
| `queued` | ready, waiting for a slot in its lane |
| `running` | in a slot |
| `done` | made its outputs |
| `failed` | its command or session failed; the error is on the node |
| `stale` | something upstream changed; it will run again |
| `needs_you` | an approval, waiting for you |
| `cancelled` | its job was cancelled |

## Edges

Edges are data: a node runs once every node it needs is done. The planner names a node's needs as it adds it, and the graph draws them as edges.

## Fingerprints and staleness

A node's fingerprint is a hash of what its output depends on: its type and spec, what each node it reads made, and the engine version. A done node whose fingerprint still matches keeps its output and isn't run again.

When a node finishes with outputs that differ from what it made before, everything downstream of it goes `stale`. When a stale node's needs are done again, it goes back to `done` if its fingerprint still matches, and otherwise runs again. So when one scene's render changes, that scene's checks, sheet, and review run again, then the video's last steps, and nothing else.

A fingerprint covers a node's spec and its inputs' outputs, not the files a command reads by itself. After you edit `scenes/<id>.html` by hand, `rapid redo` that scene's render node; what depends on it follows.

A node that makes no files (an approval, a check that only passes) hands on its own fingerprint, so a change upstream of it still reaches what comes after.

## Approvals

An approval node does no work. When its needs are done it goes to `needs_you`, and the job with it. `rapid approve <node>` (or Approve in the pane) completes it, and what waits on it moves on.

## Retries

A node that fails for a transient reason (a network error, or an overloaded or rate-limited service) is tried again in the same run, after 5 s and then after 15 s, and each retry is logged as it happens. Any other error fails the node at once.

## Redo

`rapid redo <node> [--note <text>]` runs a node again whatever its fingerprint says. The note is added to the node's spec, and an agent node reads every note in its prompt under "Notes on the last attempt". Whatever was waiting on the old outputs waits again; a cancelled branch comes back with it. A running node can't be redone.

## Cancel

`rapid cancel <job>` stops the job's running nodes and marks it `cancelled`. A done job can't be cancelled.

## The planner grows the graph

A video job starts small. The planner reads the project's files and the graph, and adds whatever comes next. It runs whenever a node in a video job finishes, and once for every running video job when the daemon starts, so a restarted daemon picks up where it was. Adding a node twice does nothing.

```text
a look to finish        finish (agent) → style check → [finish again with its reasons …] → pin in rapid.lock
a track                 listen, before the storyboard
no storyboard.json      storyboard (agent) → [direct (agent) → revise (agent) → direct …] → approve the storyboard
per Blender shot        stills → review (agent) → [revise the recipe → stills …] → frames → clips
per scene, key frame    key (agent) → still → checks → review (agent) or approve (person)
per scene, round n      [build (agent)] → render → check, sheet, micro → [review (agent)]
a round that fails      round n + 1 builds from the review's marks, until the rounds run out
out of rounds           an approval: accept the scene as it is, or redo it
every scene accepted    join → [video sheet → direct (agent)] → mix → mux → phone
the video sent back     the scenes the director names get a round more, then join again
```

In detail:

- **The look.** A video made in a look (`--look`, `look` in `CreateFilm`) starts with it: `finish the look t<n>` (agent `rapid-extractor`, lane `think`) finishes the draft without a person (a face that isn't installed gets a stand-in, noted in `taste.md`), then `check the look t<n>` (`rapid style check --write --out .rapid/look-<k>-t<n>.checks.json`). A check that fails sends its reasons to another finish, three tries in all; then the job fails with them. A check that passes is followed by `pin the look` (`rapid style use --look`), and nothing else is planned until it is pinned. A look `rapid.lock` already pins as it is (a replan, or a later video in the same look) is not finished again; one that changed since its pin is, under keys of its own.
- **The storyboard.** If `storyboard.json` is missing, a `storyboard` node (agent `rapid-storyboarder`, lane `think`) writes it, and an approval follows. Nothing else is planned until you approve it.
- **The track.** A video with `--music` (`music` in `CreateFilm`) starts with `hear the track` (a `listen` node, lane `cpu`), which writes the map the storyboarder reads. With no director, a storyboard that misses the beat goes back once to be cut to it. See [music](/docs/music).
- **Directing the storyboard.** With `--review agent` and `--director agent` (the defaults), a `direct the storyboard d<n>` node (agent `rapid-director`, lane `think`) judges the plan first and writes `reviews/storyboard-d<n>.json`, a Direction: the hook, the rhythm, each scene's move, camera, and secondary motion, variation, the redesign of every source section, and generated visuals where photos are thin. On `revise`, a `storyboard v<n+1>` node has the storyboarder answer its notes, and the director looks again, three looks in all. Code sends the plan back too, whatever the director said, while a scene names no primary move, camera, layout slot, scale, or transition out, has nothing alive while it holds, or shares its layout, primary move, or camera move with the scene before it; and while the plan is thin: a scene with no picture and no generated visual (a ground behind more than a third of the scenes doesn't count) that isn't the video's one `breath`, fewer than half the scenes with a visual where photos and clips are fewer than half the scenes, a screenshot of the source shown as it is, a scene showing the source's interface that no `redesign` names, or a key frame that doesn't say what fills its lower half (`key.lower`). The approval waits on the last look; with `--stops none` there is no approval, and the scenes wait on the last look instead.
- **Shots.** Each shot in the storyboard's `shots` renders once the storyboard is settled: stills in the `gpu` lane, a review of them, a revised recipe if it's sent back (two tries), its frames in chunks, then its clips. A scene that shows a shot isn't planned until the shot settles, and is built once its clips exist. See [Blender shots](/docs/blender-shots).
- **Key frames.** With `--keys agent` (the default), a scene built fresh first gets a `key` node (agent `rapid-builder`) that writes the scene with its hero complete at `meta.key` and no motion, then a `still` node (`rapid still --key`, writing `renders/<id>/key-k<n>.png`), then a `check` node (`rapid check --still`, writing `renders/<id>/key-k<n>.checks.json`, every scene but the storyboard's one `breath` held to 40% empty field whatever its meta's `room` says), then a `review` node (agent `rapid-reviewer`, writing `reviews/<id>-k<n>.json`), which must answer the checks' marks and read the frame into its `frame` before its verdict. `pass` lets the scene move, unless the checks hold a blocker or major or the review never read the frame: then it is a `revise`; `revise` makes another try, which builds from the review's marks, three tries in all; a third revise, or `needs_you`, adds an approval to accept the frame. With `--keys person`, an approval on the still replaces the review. With `--keys off`, or `--review none` under `agent`, there are no key frames. The first motion build waits for the key frame and is told to keep it.
- **Builds.** A scene is built (agent `rapid-builder`) on round 1 only when its file is missing, you passed `--rebuild`, or a key frame was built (the motion is still to build), and on every later round.
- **Render, check, sheet, micro.** Process nodes in the `web` lane: `rapid render`, then `rapid check`, `rapid sheet`, and (with agent review) `rapid sheet --micro`, a strip of frames 0.3 s either side of the key and of every mark the review must answer, side by side.
- **Review.** With `--review agent`, a `review` node (agent `rapid-reviewer`, lane `think`) reads the sheet, the micro strip, the checks, and the previous scene's sheet and writes `reviews/<id>-r<n>.json`. Its verdict decides: `pass` accepts the scene; `revise` starts round n + 1 while rounds remain (4 by default); `needs_you`, or `revise` on the last round, adds an approval. A review must list every earlier mark it was handed (the last round's, the key frame review's, or the director's) in `answers`; a pass that leaves one `carried`, or leaves one out, counts as a revise. With `--review none`, a scene whose checks pass is accepted, and one whose checks fail goes straight to an approval.
- **The video.** Once every scene is accepted: `join the scenes`, drawing each hand-off with the [transition](/docs/transitions) the storyboard names (`out`: a cut, or one of the named moves, from a dissolve to a glyph swallow, drawn frame by frame in Chrome from both scenes and overlapping them by its duration). Then, with the director, a `video sheet d<n>` node (`rapid video-sheet`, `renders/video-d<n>.sheet.png`: every scene's first, key, and last frames and each hand-off as a pair) and a `direct the video d<n>` node writing `reviews/video-r<n>.json`. Its notes name scenes: each scene a `revise` names gets one more round (built from the notes, its review answering them), the video joins again, and the director looks again, two looks in all; after the last look's notes are built, the video goes on without another. A person's note after the video (`rapid revise`, `revise_video`) or a replan starts the looks afresh, filed as `reviews/video-v<n>-r<d>.json` or `reviews/video-p<n>-r<d>.json`. A scene that doesn't converge in a round the director asked for takes its best round like any other, with no stops. Then `mix the sound` (each scene's cues where its picture starts, overlaps included), `the video` (the mux), and `the phone copy`.
- **Stops.** `--stops` (`stops` in `CreateFilm`) says which approvals wait for you: `all` (the default for `rapid video` and the API), `storyboard`, or `none` (the default for `new_video`). Without a stop, the storyboard goes straight on, and a key frame or a scene that would ask for you takes its best try or round instead: the least-bad verdict (`pass`, then `revise`, then `needs_you`), then the lightest marks (a blocker outweighs any number of nits), the later one on a tie. Its scene file, kept per try (`renders/<id>/key-k<n>.html`, `renders/<id>/r<n>.html`), is put back. Each choice is recorded in `.rapid/film-<job>.choices.json`, and `video_status` and `rapid job` name them. `--keys person` still asks for each key frame.

## Keys, joins, and retired nodes

A scene's nodes are keyed by a hash of its storyboard entry. When an entry changes, the scene builds again from its key frame, and the builder is told why. The video's join, mix, mux, and phone copy are keyed by the renders they join, so a scene that passes a late round joins the video again. A node the plan no longer wants (a gate for a round that was redone, or a scene a new storyboard dropped) is retired: it is cancelled and does not count against the job.

`rapid replan <job> --note <text>` (or `POST /api/videos/:job/replan`, or the `replan_video` tool) asks the storyboarder for the storyboard again. The new storyboard waits for approval when the video has stops; scenes whose entries change build again.

`rapid revise <job> --note <text>` (or `POST /api/videos/:job/revise`, or the `revise_video` tool) acts on a note on a video, done or not, and sets its stops to `none`. A scene and a time add a person's mark and start a run of rounds for that scene alone; a scene alone starts that run with the note; no scene replans. The scene's new rounds are numbered after its earlier ones (a scene that passed in round 1 revises as round 2) and keyed apart from them, so its renders stay and the video joins again.

## Watching

Every change is an event in the daemon's SQLite event log. The pane, the screening page, and `rapid events` all read the same stream. See the [HTTP API](/docs/http-api#the-event-stream).
