URL: /code/apps/media/draft · Id: `page:/code/apps/media/draft`

# Video.draft

> Writes a Video’s timeline from your records, your Brand and a Template, with a draft mark on every frame.

POST https://api.sb/&#123;your-domain&#125;/videos/draft

Writes a [Video](https://startupbuilder.dev/business/nouns/video)’s timeline from your records, your [Brand](https://startupbuilder.dev/business/nouns/brand) and a [Template](https://startupbuilder.dev/business/nouns/template), with a draft mark on every frame.

- **Action class:** None
- **Approval:** Runs on its own
- **Emits:** `Video.drafted`
- **Price:** Free

```text title="Run it · Ask"
Draft a 30-second video for the Portfolio plan: renewal notices go out 60 days early, for every owner.
```

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

const result = await $.try.Video.draft({ brand: 'brand_Leaseline', offer: 'offer_Ls1mP4qR8v' })
```

```json title="Run it · Response"
{
  "api": "https://api.sb/leaseline.app",
  "video": {
    "$id": "https://api.sb/leaseline.app/videos/video_Rn2wV7lQ4d",
    "$type": "Video",
    "status": "draft",
    "seconds": 30,
    "claims": [
      {
        "says": "$149 a month for up to 1,500 units",
        "record": "https://api.sb/leaseline.app/offers/offer_Ls1mP4qR8v"
      },
      {
        "says": "Renewal notices 60 days out",
        "record": "https://api.sb/leaseline.app/offers/offer_Ls1mP4qR8v"
      }
    ]
  },
  "action": {
    "operation": "Video.draft",
    "mode": "try",
    "status": "done"
  }
}
```

Its Inputs, Outputs, Effects and guards quote [the hub’s `Video.draft`](https://startupbuilder.dev/business/nouns/video/draft); this page adds what each Venue honors, its limits and a worked call.

## Inputs
| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `brand` | `Brand` | Required | The Brand the Video speaks for. |
| `offer` | `Offer` | `your first Offer` | The offer the Video presents. Unset: the startup’s first offer, the one its pricing page leads with. |

## Outputs
| Field | Type | What `null` means |
| --- | --- | --- |
| `video` | `Video` | Never null: the Video in status `Draft`, with a draft mark on every frame. |
| `video.status` | `string` | Never null: `draft` until `Video.finalize` clears the mark. |

## Effects
**Action class**

None: no approval rule names it, so it runs within its seat’s Permissions and Limits

**Modes**

- `try`: Writes the timeline and returns it with a frame list; nothing is rendered or stored.
- `send`: Returns a pending Action at once; `Video.drafted` fires when the timeline and its preview render.
- `do`: Waits for the preview render, about a minute for 30 seconds, and returns the Video.

**Idempotency**

Only with a key: the same `Idempotency-Key` and input return the first Action, and the same key with other input returns `409`. A repeat with the same key returns the first draft; a new key drafts again from the records as they are now.

**Saved or shared**

The Video, its timeline and the record each on-screen claim reads, so a changed price or count marks the draft stale.

**Emits**

`Video.drafted` with `{ video, brand, offer, claims }`

**Performed by**

[Content lead](https://startupbuilder.dev/agents/marketing/content-lead)

**Price**

Free on your own account: the platform charges nothing for the call.

## On each Venue
It acts on your own records and reaches no Venue, so every account answers the same way.

## What try does
Runs every check the call meets and returns the record as it would be; nothing is stored, sent or posted.

## Refuses when
| Check | Error | Fix | Then call |
| --- | --- | --- | --- |
| An invented testimonial, a synthetic person speaking as a customer, or a face or voice with no yes | [`synthetic-media`](/code/refusals#check-synthetic-media) | Use a real customer’s words with their yes, or show the product instead. | [`Content.draft`](/code/apps/sites-and-content/draft) |
| A Post with a claim that resolves to no live record | [`claims-resolve`](/code/refusals#check-claims-resolve) | Cut the claim, or name the record that backs it. | [`Content.draft`](/code/apps/sites-and-content/draft) |

```json
{
  "type": "https://startupbuilder.dev/errors/synthetic-media",
  "title": "Synthetic media",
  "status": 422,
  "detail": "An invented testimonial, a synthetic person speaking as a Customer, or a face or voice with no yes.",
  "fix": "Use a real Customer’s words with their yes, or show the product instead.",
  "next": {
    "verb": "Content.draft",
    "input": {}
  },
  "instance": "https://api.sb/leaseline.app/actions/action_1"
}
```

## When it waits
In `send` mode it returns a pending Action, `reason: 'running'` until the preview renders, and `Video.drafted` delivers the result.

```ts
await $.Watch.create({ selector: 'Video.drafted', url: 'https://leaseline.app/hooks/sb' })
```

## Example
A refusal: its offer asks for a customer testimonial with no recorded yes, so the draft refuses before a frame is written.

**Ask**

```text
Draft a 30-second video for the Portfolio plan: renewal notices go out
60 days early, for every owner, for `offer_test_testimonial`.
```

**API**

```bash
curl -X POST "https://api.sb/leaseline.app/videos/draft?mode=try" \
  -H "Authorization: Bearer $SB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "brand": "brand_test",
  "offer": "offer_test_testimonial"
}'
```

**CLI**

```bash
npx startup-builder do Video.draft --data '{
  "brand": "brand_test",
  "offer": "offer_test_testimonial"
}'
```

The CLI runs in try mode unless you pass `--do`.

**MCP**

```json
{
  "name": "do",
  "arguments": {
    "mode": "try",
    "code": "return await $.Video.draft({ brand: 'brand_test', offer: 'offer_test_testimonial' })"
  }
}
```

**SDK**

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

const result = await $.try.Video.draft({
  brand: 'brand_test',
  offer: 'offer_test_testimonial',
})
```


```json title="Response, in try mode"
{
  "type": "https://startupbuilder.dev/errors/synthetic-media",
  "title": "Synthetic media",
  "status": 422,
  "detail": "An invented testimonial, a synthetic person speaking as a Customer, or a face or voice with no yes.",
  "fix": "Use a real Customer’s words with their yes, or show the product instead.",
  "next": {
    "verb": "Content.draft",
    "input": {}
  },
  "instance": "https://api.sb/leaseline.app/actions/action_1"
}
```

To retry, send the same `Idempotency-Key`: the retry returns the first answer for $0. A new key reads again.

A `sk_test_` key answers these the same way every time, at $0 ([every test value](/code/checks#test-values)).

| Test value | Always returns | What it is for |
| --- | --- | --- |
| `brand_test` | `{"video":{"status":"draft","seconds":30}}` | Always the same 30-second draft, so the review step after it runs on a known timeline. |
| `offer_test_testimonial` | Refuses with [`synthetic-media`](/code/refusals#check-synthetic-media) | Its offer asks for a customer testimonial with no recorded yes, so the draft refuses before a frame is written. |

## Used by
- **Agent tasks:** [Ad kit](/capabilities/make-content/ad-kit)
- **Jobs:** [Make content](/capabilities/make-content)

## Related
[`Video.finalize`](/code/apps/media/finalize) · [`File.generate`](/code/apps/media/generate) · [`File.edit`](/code/apps/media/edit) · [`Voice.clone`](/code/apps/media/clone) · [`Voice.retire`](/code/apps/media/retire)