---
title: Make a whiteboard explainer from your script
description: Make a whiteboard-style explainer from your script: sketch frames from an image model, lines that draw themselves on, and a narrated voiceover.
---
Make a whiteboard explainer or a whiteboard animation from your script: black line drawings on white, arrows and underlines that draw themselves on, and a voiceover. Here your agent builds it on a timeline from your script, scene by scene.

Use invideo for this when you want the drawings, the voice and the timing on one editable timeline, so a changed sentence means one new scene, not a new video.

invideo has no library of hand-drawn art and no drawing hand. Your agent makes the drawings with an image model, such as `flux_2_lora.ballpoint_pen_sketch`, and draws simple lines and arrows itself as shapes. For the classic hand that draws each picture, use a whiteboard tool, named at the end of this page.

## Quickest path

Set up your agent first with [invideo for AI agents](/agents). Then give it this prompt.

```text
Make a whiteboard explainer in invideo from script.txt. One scene per
paragraph. Black marker drawings on white, one per scene. Draw an arrow
or underline on as each key word is said. Calm narrator. Show me the
first scenes before you make the rest.
```

In a chat app, you paste the script. In a terminal, the agent reads the file you name.

## What you need

The script sets the length. The rest is style.

- The script, split into scenes, or permission for the agent to split it.
- The style: marker, ballpoint, colored pens, plain white or a board texture.
- A voice. The agent sends previews from `search_library`.
- The shape: 16:9 for most explainers.

## Steps

The agent makes the first scenes, so you approve the style before it spends more.

<Steps>
<Step title="Split the script into scenes">
The agent splits the script into scenes, each with its line of narration and one picture to draw. It sees your yes on the scene list.
</Step>
<Step title="Voice the script">
1. The agent runs `find_models` with "narration voice", picks one, and reads its inputs with `get_model`. A good pick is `elevenlabs_text_to_speech.elevenlabs_tts_v4`, with a voice from `search_library` in `elevenlabs_text_to_speech_voices`. Your agent can search for others with `find_models`.
2. It calls `generate` with the script as `prompt`, the `voice_id` and `dry_run: true`, and tells you the price.
3. For a price of 1 credit or more it waits for your yes. Then it calls `generate` with `max_credits` set to that price and an `idempotency_key`. It gets a thread URL and, when the result lands, a sequence URL. It reads that URL with `read` and never calls `generate` again to check.
4. It prices `audio.transcript` with `dry_run` on the voice's sequence URL and tells you the price. For a price of 1 credit or more it waits for your yes. Then it starts the transcript with `max_credits` set to that price, and calls again after `retry_after_s` for the words. It sees when each key word is said.
</Step>
<Step title="Make the drawings">
1. The agent runs `find_models` with "hand-drawn whiteboard image", picks one, and reads its inputs with `get_model`. A good pick for a sketch look is `flux_2_lora.ballpoint_pen_sketch`. For a character that stays the same across scenes, a good pick is `nano_banana_2_1`, which takes `reference_images`. Your agent can search for others with `find_models`.
2. For each scene it calls `generate` with the `prompt`, `aspect_ratio` 16:9 and `dry_run: true`, and tells you the price.
3. For a price of 1 credit or more it waits for your yes. Then it calls `generate` with `max_credits` set to that price and an `idempotency_key`, like `scene-01`. It gets a thread URL and, when the result lands, one image sequence URL per scene. It reads each URL with `read` and never calls `generate` again to check.
</Step>
<Step title="Lay out the scenes">
The agent makes a 16:9 timeline with `new_timeline` and a branch with `timeline_history`. It places the voice and the drawings with `insert_media`, and fits each drawing to its line with `trim_clip`. It sees a new `state_token`.
</Step>
<Step title="Reveal each drawing">
The agent puts a plain white clip before each drawing and adds a `wipe` with `add_transition` between them, so the drawing is wiped on. For a slow push-in, it adds a `zoom_x` and a `zoom_y` lane with `add_keyframe_lane` and points with `add_keyframes`. It sees the reveal in `capture` frames.
</Step>
<Step title="Draw lines on">
For an arrow, a circle or an underline, the agent adds a shape with `add_shape`, preset `path`, an SVG path and a black stroke. In a second call, `trim_shape_path` grows the drawn part from nothing to the full line, timed to the word. It sees the line grow in `capture` frames.
</Step>
<Step title="Check, then export">
The agent calls `capture` mid-reveal and mid-line in each scene, and `check_timeline`. Then it calls `export`. It sees an export card with status `completed`.
</Step>
</Steps>

The terminal version:

```bash
invideo gen models <project-url> --query "hand-drawn whiteboard image"
invideo gen model <project-url> --model flux_2_lora.ballpoint_pen_sketch
invideo gen run <page-url> --model flux_2_lora.ballpoint_pen_sketch \
  --dry-run -i prompt=@scene-01.txt -i aspect_ratio=16:9
invideo gen run <page-url> --model flux_2_lora.ballpoint_pen_sketch \
  --max-credits <price> --idempotency-key scene-01 \
  -i prompt=@scene-01.txt -i aspect_ratio=16:9
invideo gen models <project-url> --query "narration voice"
invideo gen model <project-url> \
  --model elevenlabs_text_to_speech.elevenlabs_tts_v4
invideo gen run <page-url> \
  --model elevenlabs_text_to_speech.elevenlabs_tts_v4 --dry-run \
  -i prompt=@script.txt -i voice_id=<voice-id>
invideo gen run <page-url> \
  --model elevenlabs_text_to_speech.elevenlabs_tts_v4 \
  --max-credits <price> --idempotency-key whiteboard-voice \
  -i prompt=@script.txt -i voice_id=<voice-id>
invideo status <thread-url> --wait
invideo notebook show <thread-url>
invideo media audio.transcript <voice-sequence-url> --dry-run
invideo media audio.transcript <voice-sequence-url> \
  --max-credits <price>
invideo edit new <project-url> --name "Whiteboard v1"
invideo edit show <edit-url> --json
invideo branch new <edit-url> --name scenes-1-2 --token <state_token>
invideo edit show <branch-url> --json
invideo edit change <branch-url> --timeline T1 --token <state_token> \
  --operations @scene-01.json
invideo edit show <branch-url> --json
invideo edit change <branch-url> --timeline T1 --token <state_token> \
  --operations @draw-on.json
invideo edit capture <branch-url> --at 1,2.5,4 --out ./frames
invideo edit check <branch-url>
invideo edit export <branch-url> --format video --quality 1080 --json
invideo status <card-url> --wait
invideo notebook show <card-url> --out ./out
```

Where each value comes from:

- `<project-url>` comes from `invideo project new`, and `<edit-url>` from `invideo edit new`.
- `<page-url>` is a notebook page of the project, listed by `invideo notebook show <project-url>`. `<price>` is each dry run's `.estimated_credits`.
- `<voice-id>` comes from `invideo library search` in `elevenlabs_text_to_speech_voices`. It works only with `elevenlabs_text_to_speech.elevenlabs_tts_v4`.
- `<thread-url>` is the `url` the paid `gen run` returns. `<voice-sequence-url>` is the voice file that `notebook show` on it lists.
- `<branch-url>` is the `branch_url` that `invideo branch new` returns.
- `<state_token>` comes from `invideo edit show` on the URL the next command runs on: `<edit-url>` for `branch new`, then `<branch-url>`, read again before each change.
- `<card-url>` is `.sequences[0]` of the export answer. `invideo status` waits but saves nothing. `invideo notebook show` saves the video as `./out/export.mp4`, and the agent renames it.

Each ops file is the bare list of ops that `--operations` takes. The file behind `@scene-01.json` adds an underline shape. `at_f` and `duration_f` are frames at the timeline's frame rate.

```json
[
  { "op": "add_shape", "track": "V3", "at_f": 90, "duration_f": 150,
    "preset": "path", "d": "M 0 0 L 600 0",
    "width": 600, "height": 20, "x": 0, "y": 260,
    "fill": "#00000000",
    "stroke": { "color": "#111111", "width": 10, "cap": "round" } }
]
```

The file behind `@draw-on.json` goes in the next call and draws the line on. `c14` is the new shape clip and `<path-id>` its path, both from `invideo edit show` after the first call. `at_seconds` counts from the start of the shape clip.

```json
[
  { "op": "trim_shape_path", "clip": "c14", "path": "<path-id>",
    "at_seconds": 0, "enabled": true, "start": 0, "end": 0 },
  { "op": "trim_shape_path", "clip": "c14", "path": "<path-id>",
    "at_seconds": 1.5, "enabled": true, "start": 0, "end": 100 }
]
```

## Cost and time

Edits and exports are free. Generation and transcripts cost credits. Paid stock costs a small credit charge, or one of your workspace's iStock licenses. Your agent runs a dry run first and tells you the price before it spends anything.

- Costs credits: one drawing per scene, the voice, and the transcript for word times.
- Free: the shapes, the line draw-on, the reveals, every edit, `capture` and `export`.

Your cost grows with the number of scenes and the length of the script. A short explainer takes under an hour. See [Credits](/account/credits) for prices.

## If something goes wrong

Most problems are style drift or timing.

| Symptom | Fix |
| --- | --- |
| `get_model` or `generate` refuses the model as not found | Models retire. Run `find_models` with the job in words, use the best match, and tell the person which model you used. |
| `audio.transcript` refused with `code: "not_found"`, or CLI exit 2 | The call named a media bin asset. Pass the voice's sequence URL from the `generate` result. |
| `invideo status --wait` ends and the folder is empty | `status` does not download. Run `invideo notebook show <card-url> --out <dir>`. |
| Each drawing looks like a different artist | Use `nano_banana_2_1` with the first approved drawing in `reference_images`. |
| The wipe goes through black | `add_edge_transition` fades from black. Use `add_transition` between a white clip and the drawing. |
| A line appears at once | `trim_shape_path` needs a point with `end: 0` at the start and a later point with `end: 100`. If it still jumps, read `timeline_catalog` for `trim_shape_path`, and start a lane for the trim with `create_shape_keyframe_lane`. |
| The line draws at the wrong moment | Read the word times from `audio.transcript` and set `at_f` from them. |
| `trim_clip` refused as stale | Read with `get_timeline` and use the edge's current frame. |
| Avoid: make every scene before approval | Instead: make the first scenes and show frames. |

## Prompts to try

Each prompt is a real variation of this job.

```text
Make a whiteboard explainer for our refund policy. Blue and black marker.
Circle each number as it is said.
```

```text
Turn this script into a whiteboard video with one recurring
character, a woman in a blue jacket, in every scene.
```

```text
Redo scene 4 only. The new line is in scene-04.txt. Keep every other
scene as it is.
```

## When to use something else

For a hand that draws each picture stroke by stroke, use a whiteboard tool such as VideoScribe or Doodly. You can export from there and bring the video into invideo for voice, captions and the final cut.

## Related

- [Make a training explainer from a document, without an avatar](/how-to/training/explainer-from-a-document)
- [Voice a script or narrate a course](/how-to/training/voice-a-script-or-narrate-a-course)
- [Make a lyric video or text animation for your song](/how-to/creators/lyric-video)
- [Timeline edit operations](/reference/edit-operations)
- [Models](/reference/models)