URL: /capabilities/make-content/ad-kit · Id: `page:/capabilities/make-content/ad-kit`

# Ad kit

> One angle in, five headlines and texts, five images at each site’s sizes and a 6-second clip out, every claim checked, saved as Variants.

One angle in, five headlines and texts, five images at each site’s sizes and a 6-second clip out, every claim checked, saved as [Variants](https://startupbuilder.dev/business/nouns/variant).

```text title="Run it · Ask"
Make five ad images and one short clip for the renewals angle, spend up to $2, and show me the plan first.
```

```ts title="Run it · SDK"
import { $ } from 'startup-builder'

const result = await $.try.Ad.make({ goal: 'Make five ad images and one short clip for the renewals angle, spend up to $2, and show me the plan first.', budget: { amount: 2, currency: 'USD' } })
```

```json title="Run it · Response"
{
  "mode": "try",
  "stored": false,
  "estimate": "About 38¢ an angle",
  "sample": {
    "variant": "v_5",
    "headline": "Never miss a lease renewal again",
    "text": "Leaseline reminds you 90, 60 and 30 days out.",
    "format": "feed 1080×1080",
    "review": "passed"
  }
}
```

- **Kind:** Agent task
- **Price:** 38¢ per angle
- **Your time:** 10 minutes
- **Approval:** Runs on its own
- **Deepens:** [`process:experimentation`](https://startupbuilder.dev/workflows/processes/experimentation)

## What you get
- **1 angle** · One angle in, five headlines and texts, five images at each site’s sizes and a 6-second clip out, every claim checked, saved as Variants. · [`noun:ad`](https://startupbuilder.dev/business/nouns/ad)
- `variants[]` · Each Variant: headline, primary text, image and format, ready for Ad test setup.
- `clip` · The 6-second clip with its voiceover, made only from an image that passed.
- `review` · Assets passed, and each cut with the reason, such as “the #1 rent app” with no source.

**How long:** About 3 minutes.

## Trigger
It runs when you call it, and when message test names a winner.

```ts
on.Experiment.concluded((exp, $) => exp.decision.choice === 'ship' && $.Ad.make({ offer: exp.offer, angle: exp.winner.angle }))
```

## Steps
Your agent does these steps, one row per step; each step’s check is part of its row and its price.

| Step | Verb | Returns | Price | Approval |
| --- | --- | --- | --- | --- |
| 1. Read the angle and the offer | `Collection.query` | The angle, the buyer phrase behind it and what the offer can claim | Free | Runs on its own |
| 2. Write five headlines and texts | `Content.draft` | Five pairs at the site’s lengths: 40-character headlines, 125-character primary text | About 1¢ | Runs on its own |
| 3. Make the images | `File.generate` | Five images at each format’s size, in your brand | About 8¢ | Runs on its own |
| 4. Make the clip from the best image | `Video.draft` | A 6-second clip from the image that passed with the highest score | About 28¢ | Runs on its own |
| 5. Record the voiceover | `File.generate` | The headline read over the clip | About 1¢ | Runs on its own |
| 6. Save them as Variants | `Content.draft` | Variants ready for Ad test setup | Free | Runs on its own |

Calls: `Collection.query`, then `Content.draft`, then `File.generate`, then `Video.draft`, then `File.generate`, then `Content.draft`.

About 38¢ an angle, each time it runs, capped at $1.00.

## Example: give it a goal
> Make five ad images and one short clip for the renewals angle, spend up to $2, and show me the plan first.

The plan it shows runs 6 steps, priced before it spends anything; Run it above shows the result it returns.

## When something fails

| When | Then |
| --- | --- |
| An image fails the brand check | It is made again once, then dropped; the kit ships with the images that passed. |
| No image passes | No clip is made, and the kit returns the copy with the findings; the clip is not billed. |
| You call it twice for the same angle and formats | The same Variants return: it is idempotent per offer, angle and formats. |

Each step retries three times, then falls through to the next route; one item's failure lands in `skipped` with its reason, never failing the call. Before a step would cross `maxCost` it stops and returns what it has, with a `resume` link. Each step's idempotency key is the call's key plus the step and the item: `leaseline-ad-make-1-<step>-<item>`.

## How it is graded
A second model checks each asset against the site’s specs, your brand and the angle. It passes when each asset meets its format’s size and length, matches the brand, carries the angle, and claims only what the offer does. A miss is redone at no charge.

## Change it
| Ask | Compiles to |
| --- | --- |
| Make the story format only, with three images. | [`Ad.make`](/capabilities/make-content/ad-kit) |
| Launch the five Variants on Meta at $20 a day. | [`Campaign.launch`](/capabilities/run-ads/ad-test-setup) |
| Make a kit for the second-best angle too. | [`Ad.make`](/capabilities/make-content/ad-kit) |

```ts
import { $, workflow } from 'startup-builder'

export default workflow({
  name: 'ad-make',
  verb: 'Ad.make',
  input: { offer: 'string', angle: 'string', formats: "('feed' | 'story' | 'search')[]", images: 'integer', clip: 'boolean' },
  maxCost: 1,
  grade: { evaluator: 'Venue specs, Brand, angle', passWhen: 'size and length met; on Brand; carries the angle; no claim past the Offer', remedy: 'redo' },
  execute: async (ctx, { offer, angle, formats = ['feed', 'story'], images = 5, clip = true }) => {
    const copy = await ctx.step('copy', () => $.Content.draft({ kind: 'ad-copy', offer, angle, count: images, maxCost: 0.02 }))
    const pics = await ctx.step('images', () => $.File.generate({ kind: 'image', from: copy, formats, count: images, maxCost: 0.08 }))
    const best = pics.filter((p) => p.grade.passed).sort((a, b) => b.grade.score - a.grade.score)[0]
    const video = clip && best ? await ctx.step('clip', () => $.Video.draft({ from: best.$id, seconds: 6, voiceover: copy[0].headline, maxCost: 0.3 })) : null
    return { variants: pairUp(copy, pics), clip: video }
  },
})
```

Extend it with `extends: 'Ad.make'` and `base()`, or [compose your own](/code/compose) from these steps.

## Works with
**Plays that order it:** [Account ads](/plays/account-ads), [Creator ads](/plays/creator-ads), [House ads](/plays/house-ads), [Paid search](/plays/paid-search), [Paid social](/plays/paid-social), [Promote winners](/plays/promote-winners), [Retargeting](/plays/retargeting), [Sponsorships](/plays/sponsorships), [Video ladder](/plays/video-ladder).

It serves [Make content](/capabilities/make-content), and runs in [Paid](/strategy/paid). It deepens [`process:experimentation`](https://startupbuilder.dev/workflows/processes/experimentation).

## For agents

| Fact | Value |
| --- | --- |
| Guide | `Ad.make` |
| Guide approval | `none` |
| Goal | Make five ad images and one short clip for the renewals angle, spend up to $2, and show me the plan first. It may change counts, the order of steps, soft filters and the route a read takes, and it stops early when the goal is met. It never relaxes your words, raises its own budget, answers its own approval, calls a Play or acts outside your standing approval. |
| Trigger | `on.Experiment.concluded((exp, $) => exp.decision.choice === 'ship' && $.Ad.make({ offer: exp.offer, angle: exp.winner.angle }))` |
| Retry | Three times with backoff, then the next route; the same idempotency key never charges, sends or posts twice |
| Idempotency key | `leaseline-ad-make-1` |
| Lookups | A `find:` input asks unless exactly one record fits |

| Step | Action class | Seat rule that would hold it |
| --- | --- | --- |
| 1. Read the angle and the offer | None | None. |
| 2. Write five headlines and texts | None | None. |
| 3. Make the images | None | None. |
| 4. Make the clip from the best image | None | None. |
| 5. Record the voiceover | None | None. |
| 6. Save them as Variants | None | None. |

The amounts live once, on the hub ([approvals](https://startupbuilder.dev/agents/approvals)).

| Step | Function | Doer | Checker | Assurance | Inputs |
| --- | --- | --- | --- | --- | --- |
| 1. Read the angle and the offer | `code` | Code | Code checks each result against its declared type | `measured` | `$0.angle, $0.offer` |
| 2. Write five headlines and texts | `agentic` | The [Paid media](/capabilities/agents/paid-media) agent | A second model grades the output | `graded` | `$1, $0.formats` |
| 3. Make the images | `code` | Code | Adversarial review by a second model: Pass, or cut: an unsourced claim, an off-Brand image, text over the site’s limit | `measured` | `$2, $0.formats` |
| 4. Make the clip from the best image | `code` | Code | Code reads the result back from where it landed | `measured` | `$3.passed` |
| 5. Record the voiceover | `code` | Code | Code reads the result back from where it landed | `measured` | `$4.clip, $2.best` |
| 6. Save them as Variants | `code` | Code | Code reads the result back from where it landed | `measured` | `$3.passed, $4, $5` |

The doer is never the checker, and the weakest assurance on the path is the whole call’s.

## Under the hood

| Kind | What it touches |
| --- | --- |
| Systems | [Media](/code/apps/media), [sites and content](/code/apps/sites-and-content) |
| Nouns | [`noun:collection`](https://startupbuilder.dev/business/nouns/collection), [`noun:content`](https://startupbuilder.dev/business/nouns/content), [`noun:file`](https://startupbuilder.dev/business/nouns/file), [`noun:action`](https://startupbuilder.dev/business/nouns/action), [`noun:video`](https://startupbuilder.dev/business/nouns/video) |
| Events | `Collection.queried`, `Content.drafted`, `File.generated`, `Action.evaluated`, `Video.drafted` |

## Reference

| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `offer` | `Offer` | Required | The offer the ads sell. |
| `angle` | `string` | Required | The angle, from message test’s winner or in your words: “renewals you never miss”. |
| `formats` | `('feed' \| 'story' \| 'search')[]` | `['feed', 'story']` | The placements to size for: 1080×1080 feed, 1080×1920 story, and search text only. |
| `images` | `integer` | `5` | Images made; each must pass the brand check. |
| `clip` | `boolean` | `true` | Make one 6-second clip from the best image that passed. |
| `maxCost` | `number` | `1.00` | The most this call spends each time it runs, in USD. It stops before a step would cross it and returns what it has, with a `resume` link. |

Emits `Ad.made`. Refuses with [`max-cost`](/code/refusals#max-cost) before it spends, and with the Data calls and Accounts groups ([refusals](/code/refusals)).