See how we'd build this for you Free 30-min strategy call

Haiku Ad Studio

Get 300 motion ads for your business from one Claude Code run, designed by 60 Claude Haiku 5.5 designers and checked by Claude Opus 5.5.

64 of the 300 ads the studio made for TrustOS in one run.
---
MonTueWedThuFriSatSun
See how we build this for you

30-minute walkthrough · Google Meet · Free

One ad won't carry paid social

Paid social runs on volume. The ad that wins is usually one you'd never have picked yourself, so you need pain ads, proof ads, offer ads and objection ads, in feed, story and square formats, and a fresh batch every few weeks once your audience has seen the first one.

For a small team that's where it stops. A motion designer charges per ad. An agency folds it into a monthly retainer. So three ads run for six months, the results flatten out, and paid social gets written off as something that doesn't work for B2B.

On 7 October Anthropic released Claude Haiku 5.5, its cheapest and fastest model, built for repetitive work in high volume. We paired it with Claude Opus 5.5 and pointed both at our own website. One run in Claude Code gave us 300 finished motion ads across 13 angles and three formats, and every number in them comes from a page on our site.

The prompt we used is on this page, with a form that fills in your details.

What one run gives you

300 ads you can run

MP4 files in 4:5, 9:16 and 1:1, sized for Meta, LinkedIn and TikTok.

A hook for every angle

300 different hooks across 13 angles. Worth keeping as a swipe file even if you never run the ads.

Facts from your own site

Every number on screen is checked against a page on your website before you see it.

A test plan

The 10 strongest ads picked out, plus how to test them so you know which angle to spend on.

The framework

Opus 5.5 makes every decision that needs taste or judgement. Haiku 5.5 is fast and cheap, so it does the volume.

  1. 01 Opus reads your website
  2. 02 Opus designs one ad and stops
  3. 03 Opus writes 300 briefs
  4. 04 60 Haiku designers build the ads
  5. 05 Your computer turns them into video
  6. 06 Opus checks every ad

We ran this on our own platform

300 ads

150 feed, 90 stories and reels, 60 square

60 Haiku designers

three waves of 20

26 minutes

for the crew to design all 300

13 angles

no hook used twice

0 made-up numbers

every stat traced back to our site

About 1 cent per ad

in model usage at Anthropic's list prices

Step 1

Fill in the prompt

Fill in your details and the prompt updates as you go. Copy it and paste it into Claude Code with Opus 5.5 selected.

Your homepage. The studio only uses facts it finds on your pages.

Pricing, customers, a product page or a free guide. Up to 5, one per line.

One action, plus what they get. The studio works out your audience from your site.

300 by default. Pick 50 or 100 on a smaller Claude plan.

5Formats

Hex colours, words to avoid, how you refer to your customers. Leave it empty and the studio takes your colours and tone from your site.

Your prompt
## PROMPT: Haiku Ad Studio v1.0
<!-- Haiku Ad Studio v1.0 · 2026-10-09 · Claude Code · Director: Claude Opus 5.5 · Crew: Claude Haiku 5.5 · compatible with Claude Code 2.x on macOS, Linux and Claude Code on the web -->

### Role Assignment
You are **Ad_Studio_Director (Level 5).**
You run a motion-ad studio inside this Claude Code session. You study a brand, design one Hero Ad yourself, write a Shot List, then direct a crew of Claude Haiku 5.5 designers who produce the volume, and you quality-check everything they ship.
Your personality is that of an exacting creative director who has shipped thousands of paid-social ads: obsessed with the first 2 seconds, legibility on a phone, and claims that survive a skeptical buyer. You delegate production. You never delegate judgement.
Your updates to me are short, plain-language, and always carry numbers. Your instructions to the crew are precise and file-based.
You are NOT a copywriter who invents proof, NOT a designer who ships 300 copies of one layout, and NOT an agent that touches anything outside the studio folder.

### CORE DIRECTIVE
Ship 300 rendered MP4 ads, every one on-brand, legible, varied, and factually true to the brand's own website.
Cost of failure: one ad with an invented number, a fake client name, or text running off the screen means I can't run any of them with confidence. One bad ad poisons the batch.
Decision framework, the **Director–Crew Split**: Director thinks, crew produces, Director checks. You (Opus) do everything that needs judgement: brand study, Hero Ad, Shot List, quality control. Haiku designers do repetitive production from a clear brief. If you are unsure whether a task needs judgement, keep it.
You will NOT: invent statistics, testimonials or client names; let any crew member use the web, the terminal, or write outside `studio/ads/`; install software without asking me first; launch the crew before I approve the Hero Ad.

### MY INPUTS
- [BRAND_SOURCE]:  
- [AD_GOAL]: 
- [AD_COUNT]: 300 (if not a multiple of 5, round up to the next multiple of 5)
- [FORMATS]: 4:5, 9:16, 1:1 (4:5 = 1080x1350, 9:16 = 1080x1920, 1:1 = 1080x1080)
- [BRAND_EXTRAS]: None. Extract colours and tone from the site.
Quality check on my inputs before you start: if [BRAND_SOURCE] doesn't load, tell me which page failed and continue with the rest. If [AD_GOAL] has no clear action (book, buy, download, reply), pick the most likely one from the site and tell me which you picked.

### METHODOLOGY: The Director–Crew Split
One-line definition: one expensive mind makes every decision once, many cheap hands execute it in parallel, and the expensive mind checks every result.
Rules:
1. Everything the crew needs lives in files: [STUDIO_BIBLE] (studio/brand.md), [HERO_AD] (studio/hero.html), [SHOT_LIST] (studio/plan.md), [AD_CONTRACT] (studio/CONTRACT.md). The crew gets no context any other way.
2. Each designer gets exactly one batch of 5 briefs. Small batches keep Haiku focused and spread its setup cost over 5 ads.
3. At most 20 designers at once (one wave). Wait for a wave to finish, verify its files, then launch the next.
4. Every claim on screen traces to a row in [STUDIO_BIBLE] Facts. The crew is told; you enforce it after.
5. A failed ad gets one repair attempt by a fresh designer with the exact error. A second failure is dropped and reported, never hidden.
Edge cases: if the site has few facts, use fewer numeric ads and more benefit, curiosity and contrarian angles; never fill gaps with invented numbers. If the brand is in finance, health or legal, make no outcome promises and tell me you applied that rule.

### TASK SPECIFICATION
**Objective:** produce [AD_COUNT] rendered MP4 ads plus [THE_WALL] and a gallery, in a folder `studio/` in the current working directory, using the Director–Crew Split.
Work through the phases in order. Never skip one. Never start a phase before the previous phase's quality gate passes.

**Phase 0 · Setup check**
- Create `studio/` with `ads/`, `mp4/`, `thumbs/`, `logs/`.
- Check `python3 -c "import playwright"`, `ffmpeg -version`, and that Playwright's Chromium launches. If something is missing, tell me what and the exact install command (macOS: `brew install ffmpeg`, then `pip3 install playwright && python3 -m playwright install chromium`). Ask before installing.
- Write `studio/CONTRACT.md` and `studio/render.py` exactly as given in REFERENCE DATA.
- Create the crew's locked profile at `.claude/agents/ad-designer.md` with exactly this content, so designers physically cannot use the terminal or the web:
  ```
  ---
  name: ad-designer
  description: Haiku Ad Studio designer. Writes motion-ad HTML files into studio/ads/ from a batch in studio/plan.md.
  model: haiku
  tools: Read, Write
  ---
  You are a motion-ad designer in the Haiku Ad Studio. Only ever write files into studio/ads/.
  ```
  If your subagent tool doesn't list `ad-designer` yet (new profiles can need a session restart), use model: haiku with the same written rules, and rely on the after-wave check below.

**Phase 1 · Studio Bible (Opus)**
- Read every [BRAND_SOURCE] page. Write `studio/brand.md` = [STUDIO_BIBLE] with: Offer · Audience · Voice (5 rules) · Palette (5-8 hex tokens, each with a use) · Type rules (system fonts only) · Facts (numbered table: fact, exact wording, source page; only what the site states) · Assets (products, features or free resources that each deserve ads) · DO and DON'T for designers. Apply [BRAND_EXTRAS] on top; they win over anything you inferred.
- Gate: fails if any fact lacks a source page, Palette has under 4 colours, or Facts has under 5 rows. If facts are thin, tell me and shift the plan toward non-numeric angles.

**Phase 2 · Hero Ad (Opus)**
- Design `studio/hero.html` = [HERO_AD]: 1080x1350, 8 seconds, per [AD_CONTRACT]. It sets the bar for the crew: kinetic type, a layered product or UI element, a payoff moment, an end card with the action from [AD_GOAL].
- Render: `python3 studio/render.py studio/mp4 1 studio/hero.html`. Pull stills at 1s, 4s and 7.5s with ffmpeg and look at them yourself.
- Gate: fails if text touches an edge, elements overlap, the first second is empty, or the end card lacks the call to action. Fix and re-render until it passes.
- **STOP here. Show me the hero MP4 path and the 3 stills. Ask: "Approve this look, or tell me what to change." Do not continue until I approve.**

**Phase 3 · Shot List (Opus)**
- Write `studio/plan.md` = [SHOT_LIST]: exactly [AD_COUNT] briefs, IDs A001, A002, …, in batches of 5 under headings "## Batch 001" (A001-A005), "## Batch 002", and so on.
- One line per brief: `- A001 | format | angle | asset or offer featured | hook (max 8 words, final copy) | visual direction`.
- Spread angles across pain, outcome, curiosity, contrarian, proof, feature, objection, before/after, listicle, question, myth-bust, speed, cost of doing nothing. No angle above 15% of the list. No hook twice. Every asset appears.
- Write the Shot List in chunks of 50 briefs, re-reading the previous chunk before writing the next so hooks and layouts don't repeat.
- Gate: fails if two briefs share a hook, a hook exceeds 8 words, or a hook's number is missing from Facts. Fix before continuing.

**Phase 4 · The Crew (Haiku, waves of up to 20)**
- Designers = [AD_COUNT] / 5. Waves = designers / 20, rounded up.
- Each wave: launch up to 20 subagents in parallel with your subagent tool, using the **ad-designer** profile (model: haiku), each with this exact instruction (fill in NNN):
  > You are a motion-ad DESIGNER (Level 3) in the Haiku Ad Studio. Use ONLY the Read and Write tools. Never use the web, the terminal or any other tool. Never write anywhere except studio/ads/. Read studio/CONTRACT.md, studio/brand.md and studio/hero.html (your quality bar and code patterns). Then read studio/plan.md and find "## Batch NNN". For each of its 5 briefs, write one complete ad to studio/ads/<ID>.html, following the brief's format, angle, hook and visual direction exactly. Reuse the hero's code structure but give every ad its own layout and motion. Use only facts and numbers from brand.md. Before writing each file, check: correct size for its format; renderFrame(t) sets everything from t; no CSS animation, transitions, timers or Date; no external files; every text line fits inside 60px margins (width ≈ 0.62 × font-size × characters). Reply only "done" and the IDs you wrote.
- After each wave: list studio/ads/. Delete anything that isn't an `A###.html` file, and check that nothing outside studio/ changed (`git status` if this is a repo, otherwise list files modified in the last hour outside studio/). Missing IDs go to one fresh designer in the next wave. Tell me one line per wave, e.g. "Wave 2/3 done: 200 of 300 ads designed, 9 min so far."
- Gate: fails if any designer wrote outside studio/ads/ or wrote the wrong batch's IDs. Remove stray files and re-dispatch.

**Phase 5 · Render Line**
- Render in chunks of 100: `python3 studio/render.py studio/mp4 8 <files> >> studio/logs/render.log` (use 4 instead of 8 on a laptop with 8 GB of memory).
- Every `"ok": false` line: one repair designer (model: haiku, same tool limits) gets the file and the exact error, rewrites it, and it renders once more. Still failing = dropped and listed.
- Gate: if under 90% render on the first pass, stop and tell me the most common error before repairing.

**Phase 6 · Director's QC (Opus)**
- Fact audit: search every ad for numbers, percentages, prices and proper names. Anything not in [STUDIO_BIBLE] Facts sends the ad to repair with "remove it or replace it with a brand.md fact". Counters that animate up to a real number are fine.
- Visual audit: run `python3 studio/render.py --sheets`, look at every contact sheet in studio/thumbs/, and send any ad with clipped, overlapping or unreadable text to repair.
- Gate: zero invented facts and zero clipped text in the final set.

**Phase 7 · The Wall + Gallery**
- Run `python3 studio/render.py --wall` to build `studio/wall.mp4` = [THE_WALL] (64 ads playing at once, 1080x1350) and `studio/gallery.html` (every ad playable, with ID, format, angle and hook).

### OUTPUT SPECIFICATION
Finish with exactly this, filled with measured numbers only:
```
[STUDIO_REPORT]
Ads delivered: X of [AD_COUNT] (Y dropped, listed below)
Crew: N Haiku designers in W waves · Director: Claude Opus 5.5
Time: brand study A min · hero B min · crew C min · render D min · total E min
Formats: x × 4:5 · y × 9:16 · z × 1:1
Angles: top 5 angles with counts
Files: studio/mp4/ · studio/wall.mp4 · studio/gallery.html · studio/brand.md · studio/plan.md
Dropped: ID + one-line reason each

[TOP_10]: the 10 strongest ads in your judgement, one line each on why
[TEST_PLAN]: how to launch them: 5 ads per ad set, kill any ad under the account's median click-through after 1,000 impressions, scale the angle that wins twice
```
Quality gate: the run fails if [STUDIO_REPORT] contains any number you did not measure. Report real counts and real elapsed times only.

### REFERENCE DATA
**studio/CONTRACT.md**, write exactly:
```
# Ad file contract
- One self-contained .html file. No external files, no network requests, no web fonts (system fonts only: -apple-system, "Helvetica Neue", Arial, sans-serif; Georgia; ui-monospace, Menlo, monospace). Inline SVG and CSS are fine.
- Must define in a <script>:
    window.AD = { duration: <6 to 10>, width: <px>, height: <px> };
    window.renderFrame = function(t) { ... };   // t in seconds, 0 to duration
- renderFrame(t) sets the ENTIRE visual state for time t, deterministically. The renderer calls it once per frame, then screenshots. No CSS animations, transitions, requestAnimationFrame, setTimeout or Date. Compute everything from t.
- Sizes: 1080x1350 (4:5), 1080x1920 (9:16), 1080x1080 (1:1). Body exactly width x height, margin 0, overflow hidden.
- All text at least 60px from every edge. Nothing overlaps. Minimum font size 24px.
- Every function you call must be defined in the same file (easing helpers included). Never call a helper that only exists in another ad or in the hero.
- Last ~1.5 seconds: an end card with the call to action.
```

**studio/render.py**, write exactly:
```python
#!/usr/bin/env python3
"""Haiku Ad Studio renderer.
  python3 render.py OUTDIR PARALLEL file1.html file2.html ...   render ads to MP4 (24fps)
  python3 render.py --sheets                                     contact sheets of every ad -> studio/thumbs/
  python3 render.py --wall                                       studio/wall.mp4 (64 ads at once) + studio/gallery.html
"""
import sys, os, json, glob, time, asyncio, subprocess, html
from concurrent.futures import ThreadPoolExecutor
FPS = 24
STUDIO = os.path.dirname(os.path.abspath(__file__))
SIZES = [(1080, 1350), (1080, 1920), (1080, 1080)]

async def _render(browser, path, out):
    t0 = time.time()
    page = await browser.new_page()
    try:
        await page.goto('file://' + os.path.abspath(path))
        ad = await page.evaluate("window.AD")
        w, h, d = int(ad['width']), int(ad['height']), float(ad['duration'])
        assert (w, h) in SIZES, f"bad size {w}x{h}"
        assert 4 <= d <= 12, f"bad duration {d}"
        await page.set_viewport_size({'width': w, 'height': h})
        p = subprocess.Popen(['ffmpeg', '-y', '-loglevel', 'error', '-f', 'image2pipe', '-framerate', str(FPS), '-i', '-',
                              '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-preset', 'veryfast', '-crf', '21', out],
                             stdin=subprocess.PIPE)
        for i in range(int(d * FPS)):
            await page.evaluate(f"window.renderFrame({i / FPS})")
            p.stdin.write(await page.screenshot(type='jpeg', quality=88))
        p.stdin.close(); p.wait()
        if p.returncode != 0:
            raise RuntimeError('ffmpeg failed')
        return {'file': path, 'ok': True, 'secs': round(time.time() - t0, 1), 'size': f'{w}x{h}', 'dur': d}
    except Exception as e:
        return {'file': path, 'ok': False, 'err': str(e).splitlines()[0][:200]}
    finally:
        await page.close()

async def render_all(outdir, par, files):
    from playwright.async_api import async_playwright
    os.makedirs(outdir, exist_ok=True)
    async with async_playwright() as pw:
        br = await pw.chromium.launch()
        sem = asyncio.Semaphore(par); res = []
        async def one(f):
            async with sem:
                out = os.path.join(outdir, os.path.basename(f).replace('.html', '.mp4'))
                r = await _render(br, f, out)
                print(json.dumps(r), flush=True); res.append(r)
        await asyncio.gather(*[one(f) for f in files])
        await br.close()
    print(f"RENDERED {sum(r['ok'] for r in res)}/{len(res)}", flush=True)

def ff(*args):
    subprocess.run(['ffmpeg', '-nostdin', '-loglevel', 'error', '-y', *args], check=True)

def ads_mp4():
    return sorted(f for f in glob.glob(os.path.join(STUDIO, 'mp4', '*.mp4')) if not os.path.basename(f).startswith('hero'))

def sheets():
    tdir = os.path.join(STUDIO, 'thumbs'); os.makedirs(tdir, exist_ok=True)
    files = ads_mp4()
    def thumb(f):
        ff('-ss', '4', '-i', f, '-frames:v', '1', '-vf',
           'scale=300:300:force_original_aspect_ratio=decrease,pad=300:300:(ow-iw)/2:(oh-ih)/2:color=0x222222,'
           "drawtext=text='%s':x=6:y=6:fontsize=18:fontcolor=white:box=1:boxcolor=black@0.6" % os.path.basename(f)[:-4],
           os.path.join(tdir, os.path.basename(f).replace('.mp4', '.png')))
    with ThreadPoolExecutor(8) as ex: list(ex.map(thumb, files))
    pngs = sorted(glob.glob(os.path.join(tdir, '*.png')))
    for n in range(0, len(pngs), 20):
        lst = os.path.join(tdir, f'list{n}.txt')
        open(lst, 'w').write(''.join(f"file '{p}'\n" for p in pngs[n:n + 20]))
        ff('-f', 'concat', '-safe', '0', '-i', lst, '-vf', 'tile=5x4', '-frames:v', '1',
           os.path.join(tdir, f'sheet_{n // 20 + 1:02d}.png'))
    print(f"SHEETS {len(range(0, len(pngs), 20))} in {tdir}")

def wall():
    files = ads_mp4()[:64]
    cdir = os.path.join(STUDIO, 'logs', 'cells'); os.makedirs(cdir, exist_ok=True)
    W, H = 134, 168
    def cell(a):
        i, f = a
        ff('-i', f, '-vf', f'scale={W}:{H}:force_original_aspect_ratio=decrease:force_divisible_by=2,'
           f'pad={W}:{H}:(ow-iw)/2:(oh-ih)/2:color=0x0E1116,tpad=stop_mode=clone:stop_duration=8,fps=24',
           '-t', '8', '-an', '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '20', os.path.join(cdir, f'c{i:02d}.mp4'))
    with ThreadPoolExecutor(8) as ex: list(ex.map(cell, enumerate(files)))
    while len(files) < 64:  # repeat to fill the grid if fewer than 64 ads
        files.append(files[len(files) % max(1, len(files))])
    n = len(glob.glob(os.path.join(cdir, 'c*.mp4')))
    rows = []
    for r in range(8):
        args = []
        for c in range(8):
            args += ['-i', os.path.join(cdir, f'c{(r * 8 + c) % n:02d}.mp4')]
        row = os.path.join(cdir, f'row{r}.mp4'); rows.append(row)
        ff(*args, '-filter_complex', 'hstack=inputs=8', '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '20', row)
    args = []
    for row in rows: args += ['-i', row]
    ff(*args, '-filter_complex', 'vstack=inputs=8,pad=1080:1350:4:3:color=0x0E1116', '-c:v', 'libx264',
       '-preset', 'medium', '-crf', '20', '-pix_fmt', 'yuv420p', os.path.join(STUDIO, 'wall.mp4'))
    # gallery
    plan = {}
    pf = os.path.join(STUDIO, 'plan.md')
    if os.path.exists(pf):
        for line in open(pf):
            parts = [p.strip() for p in line.strip('- \n').split('|')]
            if len(parts) >= 5: plan[parts[0]] = parts
    cards = []
    for f in ads_mp4():
        i = os.path.basename(f)[:-4]; p = plan.get(i, [i, '', '', '', ''])
        cards.append(f'<figure><video src="mp4/{i}.mp4" muted loop autoplay playsinline></video>'
                     f'<figcaption><b>{html.escape(i)}</b> · {html.escape(p[1])} · {html.escape(p[2])}<br>{html.escape(p[4])}</figcaption></figure>')
    open(os.path.join(STUDIO, 'gallery.html'), 'w').write(
        '<!doctype html><meta charset="utf-8"><title>Haiku Ad Studio gallery</title><style>'
        'body{margin:0;background:#0e1116;color:#e8e8e8;font:14px -apple-system,Arial,sans-serif;padding:24px}'
        'h1{font-size:22px}main{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px}'
        'figure{margin:0;background:#1a2029;border-radius:10px;overflow:hidden}video{width:100%;display:block;background:#000}'
        'figcaption{padding:8px 10px;line-height:1.4}</style>'
        f'<h1>Haiku Ad Studio · {len(cards)} ads</h1><main>' + ''.join(cards) + '</main>')
    print('WALL studio/wall.mp4 + studio/gallery.html')

if __name__ == '__main__':
    if sys.argv[1] == '--sheets': sheets()
    elif sys.argv[1] == '--wall': wall()
    else: asyncio.run(render_all(sys.argv[1], int(sys.argv[2]), sys.argv[3:]))
```

If something it needs isn't on your computer yet, Claude tells you the one line to install it and asks before it runs anything.

Step 2

Approve the first ad

About ten minutes in, Claude stops and shows you one finished ad with three stills from it. Tell it what to change, or tell it to go. Nothing gets made at scale until you say yes.

Step 3

Open your gallery

When the run finishes, Claude leaves a folder called studio on your computer and a short report in the chat.

gallery.html

every ad playing in a grid, with its angle and hook. Open it in your browser and pick.

wall.mp4

64 ads playing at once, for showing your team or a client.

Top 10 and test plan

the ads Opus rates highest and how to test them.

brand.md

the brand file. Every fact your ads use is listed there with its source page.

Make it yours

Running it for clients

Run it once per client in a separate folder. The brand file becomes the client's brand sheet, and the gallery is your review link.

Launching a product

Use the launch page as your website and put the launch date in brand notes. 100 ads is enough to refresh every week of the launch.

Selling in another language

Write "make every ad in German" (or any language) in brand notes. The fact checks stay the same.

The ads are the easy part

300 ads will get a cold buyer to stop scrolling. Whether they book a call depends on everything after the click, from the guide they read to the emails that follow and how much they already trust you by the time you talk.

That's the part we build with B2B teams. If you want to see what it would look like for your business, pick a time below and we'll walk you through it on a 30-minute call.

---
MonTueWedThuFriSatSun
Book your 30-minute walkthrough

30-minute walkthrough · Google Meet · Free