---
title: Test hooks on one ad, with branches
description: Many hook variants, or ad creative variations, from one master ad: one branch per hook, a new first line or first clip, and a named file for each.
---
You get many versions of one ad, each with a different hook: the first line, the first clip, or both. This is hook testing, also called creative testing or ad variations. The body of the ad stays the same, so the test shows which opening works. Each version is a named MP4 that you upload to your ad platform.

Use invideo for this when you want every variant to come from one master timeline: each hook lives on its own branch, a fix to the master body is one edit, and you can open any branch in the editor to check it.

Making ad variants is ordinary marketing work, and your agent does it. The agent never uploads, switches on or pays for an ad. You run the test in the ad platform.

## Quickest path

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

```text
Here is my master ad <edit-url> and 12 hooks in hooks.txt, one per
line. Make one branch per hook. Change the first text line to the hook,
and use the first clip listed next to each hook. Export each one with
the file name serum-h01 to serum-h12 and give me the list.
```

In a chat app, you paste the hooks into the chat. In a terminal, the agent reads a text or CSV file you name.

## What you need

A good master and a clean list make the test easy to read.

- **A master ad.** A finished timeline in invideo, with the hook as its own text clip in the first seconds.
- **The hooks.** One per line: the words, and the first clip for that hook if it changes.
- **Hook clips.** Any new opening clips, as MP4 or MOV, or stock you want to search.
- **A naming scheme.** A short ad name and a hook number, such as `serum-h01`. Add the angle if you track it, such as `serum-h01-price`.

## Steps

The agent keeps main untouched. Every hook is a branch off the master.

<Steps>
<Step title="Read the master">
The agent reads the timeline with `get_timeline` in `mode` window. It finds the hook text clip and the first video clip, and notes their ids and frames. Expected result: the ids of the hook text and the first clip, and the `state_token`.
</Step>
<Step title="Save a checkpoint of the master">
The agent saves a checkpoint with `timeline_history` `checkpoint`, titled "master". Each branch then starts from it: `create_branch` with `checkpoint_slug` set to that checkpoint, or `invideo branch new --checkpoint` on the CLI. Without it, a branch starts from the current state of the branch you are on. Expected result: the checkpoint shows in `timeline_history` `list`.
</Step>
<Step title="Make one branch per hook">
For each hook, the agent runs `timeline_history` `create_branch` with a name such as `serum-h01`. Expected result: a branch URL per hook, of the shape `…/tree/serum-h01/seq-9.1`.
</Step>
<Step title="Change the hook line">
On each branch, the agent changes the hook words with `set_text` on the hook clip. Expected result: the new hook on screen in the first seconds.
</Step>
<Step title="Change the first clip">
When a hook has its own opening clip, the agent adds the file with `add_media`. It puts it in place of the first clip with `place_media` in `mode` replace, which keeps the old clip's length. Expected result: the new clip first, and the body of the ad unchanged.
</Step>
<Step title="Generate a hook clip you do not have">
The agent runs `find_models` with the job in words, picks one, and reads its inputs with `get_model`. A good pick is `seedance_2_5.frames`. It calls `generate` with `dry_run: true` and tells you the price. Then it calls `generate` with `max_credits` set to that price and an `idempotency_key`. Expected result: a sequence URL, which the agent reads with `read` and places first with `place_media`.
</Step>
<Step title="Check every variant, then export">
The agent runs `capture` at the first and the third second of each branch, and `check_timeline`. It checks that the hook is readable and clear of the app buttons. Then it runs `export` on each branch URL with `format` video, `file_name` set from the naming scheme, and its own `idempotency_key`. Expected result: one export card per hook, each with status `completed`, and a list of names for you.
</Step>
</Steps>

The same job from a terminal, for one hook. The agent repeats it for each line of the file.

```bash
invideo agent register <project-url> --name <name>
export INVIDEO_AS=<name>
invideo edit show <edit-url> --mode window --start-seconds 0 \
  --end-seconds 5
invideo branch checkpoint <edit-url> --title "master" --token <state_token>
invideo edit show <edit-url> --json
invideo branch list <edit-url>
invideo branch new <edit-url> --name serum-h01 --token <state_token> \
  --checkpoint <checkpoint>
invideo gen models <project-url> --query "short opening shot for an ad"
invideo gen model <project-url> --model seedance_2_5.frames
invideo gen run <page-url> --model seedance_2_5.frames --dry-run \
  -i prompt="close-up, serum drop on skin" -i aspect_ratio=9:16 -i duration=4
invideo gen run <page-url> --model seedance_2_5.frames \
  --max-credits <price> --idempotency-key serum-h01-clip \
  -i prompt="close-up, serum drop on skin" -i aspect_ratio=9:16 -i duration=4
invideo edit show <branch-url> --json
invideo edit change <branch-url> --timeline T1 --token <state_token> \
  --operations @hook-01.json
invideo edit capture <branch-url> --at 1,3 --out ./frames/h01
invideo edit export <branch-url> --format video --quality 1080 \
  --file-name serum-h01 --idempotency-key serum-h01-v1 --json
invideo status <card-url> --wait
invideo notebook show <card-url> --out ./exports
mv ./exports/export.mp4 ./exports/serum-h01.mp4
```

Where the values come from: `<edit-url>` is the master's timeline URL, from `invideo edit new`. `<project-url>` comes from `invideo project new`. `<name>` is the name you choose in `invideo agent register`, and `<page-url>` is a notebook page from `invideo notebook show <project-url>`. `<state_token>` comes from the `invideo edit show` right before each command: the first one, then the one after the checkpoint, then the one on the branch, because the main token is stale there. `<checkpoint>` is the name `invideo branch list` gives the checkpoint. `<branch-url>` is the `branch_url` that `invideo branch new` returns. `<price>` is the dry run's `estimated_credits`. `<card-url>` is `.sequences[0]` of what the export returns. The ops file `hook-01.json` is a bare list: the `operations` list of the MCP call below.

The key MCP call on one branch changes the hook line and swaps the first clip. `<edit-url>` in the prompt above is the master's URL. Here `<state_token>` is the branch's token from `get_timeline`, the ids `c9` and `c1` come from the same read, and `<sequence-url>` is the hook clip's sequence URL from `add_media` or from the generation.

```json
{
  "url": "<branch-url>",
  "timeline_id": "T1",
  "expected_state_token": "<state_token>",
  "operations": [
    { "op": "set_text", "clip": "c9",
      "text": "I stopped buying three products for this one" },
    { "op": "place_media", "asset": "<sequence-url>",
      "track": "V1", "at_f": 0, "source_start_seconds": 0,
      "source_end_seconds": 3, "mode": "replace", "replace_clip": "c1",
      "linked_audio": false }
  ]
}
```

The export for that branch sets the file name in `options`. The name shows on the card. A file saved with `invideo notebook show` is still `export.mp4`.

```json
{
  "url": "<branch-url>",
  "format": "video",
  "idempotency_key": "serum-h01-v1",
  "options": { "quality": "1080", "file_name": "serum-h01" }
}
```

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

- **Free:** branches, checkpoints, the text and clip changes, the checks and every export.
- **Costs credits:** a stock clip for a new opening, and any generated opening clip or voice line.

A list of hooks takes a few minutes of edits, plus the export time for each. For a long list, the agent runs it in parts and reports after each part. See [Credits and what they cost](/account/credits) for prices.

## If something goes wrong

Most problems are a stale token between branches, or a hook clip that changes the ad's length.

| 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. |
| Exit code 6, or `status` is `held` | A safety review, not a refusal. Wait, then run `invideo status <url> --wait` again. Do not resend. If it ends `failed`, change the input the review flagged. |
| The exports folder is empty | `invideo status` does not download. Run `invideo notebook show <card-url> --out ./exports`. |
| You want all variants side by side on one branch | Copy the timeline with `duplicate_timeline` instead, one copy per hook. |
| The hook needs a spoken line | Make it with `elevenlabs_text_to_speech.elevenlabs_tts_v4` and a `voice_id` from `search_library` in `elevenlabs_text_to_speech_voices`, priced and run the same way as a clip. |
| A new clip must push the rest of the ad later | Use `insert_media` with `ripple` true instead of `place_media`. |
| CLI exit 3, or a stale `expected_state_token` | Each branch has its own token. Read that branch with `get_timeline` before the edit. |
| `set_text` refused on a caption clip | The caption has several slides. Use `set_caption_slide_text` for the slide with the hook. |
| The new first clip is longer and the ad grew | Use `place_media` in `mode` replace, which keeps the old clip's length. |
| Two exports have the same name | Give each export its own `file_name` and its own `idempotency_key`. |
| `timeout` on an export | Do not export again. Read the export card URL, or run `invideo status`. |
| Avoid: editing main for each hook | Instead: one branch per hook, off the master checkpoint. |
| Avoid: rebuilding the whole ad per hook | Instead: change only the hook text and the first clip. |

## Prompts to try

Each prompt is a real variation of the job.

```text
Make 20 hook variants of my ad from hooks.csv. Columns: hook, first
clip. Name them acme-h01 to acme-h20. Run it in parts of five and
show me frames after each part.
```

```text
For my 3 best hooks, make a 9:16 and a 1:1 version each, so 6 files.
Name them with the hook number and the shape.
```

```text
Find a stock clip for each of these 10 hooks, put it first, and tell
me the price of the stock before you add it.
```

## When to use something else

invideo makes the variants. It does not run the test, split the budget or read the results. Use your ad platform's own testing tool, such as Meta Ads Manager or TikTok Ads Manager, and match the file names in its report.

## Related

- [Make performance ads for Meta, TikTok and YouTube, named and iterated from results](/how-to/marketing/performance-ads)
- [Resize one master ad for every placement, with safe zones](/how-to/marketing/every-placement-from-one-master)
- [Make a UGC ad from product photos with an AI actor](/how-to/marketing/ugc-ad-with-ai-actor)
- [Localize a winning ad with the same voice and lip-sync](/how-to/marketing/localize-a-winning-ad)
- [Turn a CSV into many videos with a script](/how-to/developers/csv-to-videos)