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

# Launch kit

> Your offer in, every launch site’s fields filled and checked against its limits, with screenshots at its sizes and the best slot to post.

Your [offer](https://startupbuilder.dev/business/nouns/offer) in, every launch site’s fields filled and checked against its limits, with screenshots at its sizes and the best slot to post.

```text title="Run it · Ask"
Get Leaseline’s launch kit ready for Product Hunt, Hacker News and Reddit next Tuesday, spend up to $1, and show me the plan first.
```

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

const result = await $.try.Content.compile({ goal: 'Get Leaseline’s launch kit ready for Product Hunt, Hacker News and Reddit next Tuesday, spend up to $1, and show me the plan first.', budget: { amount: 1, currency: 'USD' } })
```

```json title="Run it · Response"
{
  "mode": "try",
  "stored": false,
  "estimate": "About 0.16¢ per 8 Venues",
  "sample": {
    "venue": "Product Hunt",
    "tagline": "Rent and renewals, off the spreadsheet",
    "check": "41 of 60 characters",
    "missing": "first comment",
    "slot": "Tue 12:01 PT"
  }
}
```

- **Kind:** Workflow
- **Price:** 0.16¢ per 8 sites
- **Your time:** 15 minutes
- **Approval:** Runs on its own
- **Deepens:** [`workflow:compile-demand-kit`](https://startupbuilder.dev/workflows/processes/launches/compile-demand-kit)

## What you get
- **8 sites** · Your offer in, every launch site’s fields filled and checked against its limits, with screenshots at its sizes and the best slot to post. · [`noun:content`](https://startupbuilder.dev/business/nouns/content)
- `venues[].fields` · Each field the site asks for, filled from your records: name, tagline, topics, description, first comment.
- `venues[].checks` · Each field against the site’s rule, such as a Product Hunt tagline of 60 characters or fewer, and pass or fail.
- `venues[].missing` · Fields your records cannot fill, such as the maker’s first comment; Launch day writes these.
- `assets` · Screenshots, the logo and the social image, rendered at each site’s sizes.
- `calendar` · The slot for each site from a declared table: Product Hunt at 12:01 PT on a Tuesday to Thursday, Show HN at 8 to 10 ET on a weekday.

**How long:** About 20 seconds.

## Trigger
It runs when you call it, and each time the offer is published again.

```ts
on.Offer.published((offer, $) => $.Content.compile({ kind: 'launch-kit', offer: offer.$id }))
```

## 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 offer, the brand and the pages | `Collection.query` | Every field your records hold for the kit | Free | Runs on its own |
| 2. Read each site’s current field limits | [`Page.audit`](/code/data/pages-and-sites/audit-page) | Field lengths, link formats and image sizes, read fresh when the stored ones are older than 90 days | About 0.16¢ | Runs on its own |
| 3. Render screenshots at each site’s sizes | `File.generate` | Screenshots, the logo and the social image as Files | Free | Runs on its own |
| 4. Fill and check each site’s fields | `Collection.query` | Each field filled, checked against its rule, or listed under `missing` | Free | Runs on its own |
| 5. Set the slot for each site | `Collection.query` | The post time per site from the declared table | Free | Runs on its own |

Calls: `Collection.query`, then `Page.audit`, then `File.generate`, then `Collection.query`.

About 0.16¢ per 8 sites, each time it runs, capped at 25¢.

## Example: give it a goal
> Get Leaseline’s launch kit ready for Product Hunt, Hacker News and Reddit next Tuesday, spend up to $1, and show me the plan first.

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

## When something fails

| When | Then |
| --- | --- |
| A site’s stored field limits are older than 90 days and its page cannot be read | The site compiles with the stored limits and is flagged `limits-unverified`. |
| A field fails its rule | The check names the rule and the length; it never trims or rewrites your words. |
| You call it again for the same offer version | It returns the same kit at $0: it is keyed on offer, sites and offer version. |

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-content-compile-1-<step>-<item>`.

## Change it
| Ask | Compiles to |
| --- | --- |
| Write the missing fields in my voice. | [`Post.launch`](/capabilities/earn-first-links/launch-day) |
| Add Hacker News and Indie Hackers to the sites. | [`Content.compile`](/capabilities/make-content/launch-kit) |
| Make a listing kit for directories from the same fields. | [`Post.submit`](/capabilities/earn-first-links/directory-submissions) |

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

export default workflow({
  name: 'compile-demand-kit',
  verb: 'Content.compile',
  input: { kind: "'launch-kit'", offer: 'string', venues: 'string[]' },
  maxCost: 0.25,
  execute: async (ctx, { offer, venues = defaultLaunchVenues }) => {
    const src = await ctx.step('read', () => $.Collection.query({ from: ['offer', 'brand', 'page'], where: `offer = '${offer}'` }))
    const limits = await ctx.step('limits', () => $.Page.audit({ urls: venueLimitUrls(venues, { olderThan: '90d' }), fields: ['content'], maxCost: 0.01 }))
    const assets = await ctx.step('render', () => $.File.generate({ kind: 'screenshots', sizes: venueSizes(venues), from: src.pages }))
    return venues.map((v) => ({ venue: v, ...fillAndCheck(v, src, limits), assets, at: bestSlot(v) }))   // declared tables
  },
})
```

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

## Works with
**Plays that order it:** [Reuse what you already published](/plays/archive-repurposing), [Category kit](/plays/category-kit), [Data stories](/plays/data-stories), [Guest on podcasts and newsletters](/plays/guest-circuit), [Launch sites](/plays/launch-sites), [Launch week](/plays/launch-week), [Deposits before you build](/plays/reserve-drop).

It serves [Make content](/capabilities/make-content), and runs in [Launch and search](/strategy/launch-and-search), [Borrowed](/strategy/borrowed). It deepens [`workflow:compile-demand-kit`](https://startupbuilder.dev/workflows/processes/launches/compile-demand-kit).

## For agents

| Fact | Value |
| --- | --- |
| Guide | `Content.compile` |
| Guide approval | `limits` |
| Goal | Get Leaseline’s launch kit ready for Product Hunt, Hacker News and Reddit next Tuesday, spend up to $1, 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.Offer.published((offer, $) => $.Content.compile({ kind: 'launch-kit', offer: offer.$id }))` |
| Retry | Three times with backoff, then the next route; the same idempotency key never charges, sends or posts twice |
| Idempotency key | `leaseline-content-compile-1` |
| Lookups | A `find:` input asks unless exactly one record fits |

| Step | Action class | Seat rule that would hold it |
| --- | --- | --- |
| 1. Read the offer, the brand and the pages | None | None. |
| 2. Read each site’s current field limits | None | None. |
| 3. Render screenshots at each site’s sizes | None | None. |
| 4. Fill and check each site’s fields | None | None. |
| 5. Set the slot for each site | None | None. |

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

| Step | Function | Doer | Checker | Assurance | Inputs |
| --- | --- | --- | --- | --- | --- |
| 1. Read the offer, the brand and the pages | `code` | Code | Code checks each result against its declared type | `measured` | `$0.offer` |
| 2. Read each site’s current field limits | `code` | Code | Code checks each result against its declared type | `measured` | `$0.venues` |
| 3. Render screenshots at each site’s sizes | `code` | Code | Code reads the result back from where it landed | `measured` | `$1.pages, $2.sizes` |
| 4. Fill and check each site’s fields | `code` | Code | Code checks each result against its declared type | `measured` | `$1, $2` |
| 5. Set the slot for each site | `code` | Code | Code checks each result against its declared type | `measured` | `$2.venues` |

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) |
| Nouns | [`noun:collection`](https://startupbuilder.dev/business/nouns/collection), [`noun:page`](https://startupbuilder.dev/business/nouns/page), [`noun:file`](https://startupbuilder.dev/business/nouns/file) |
| Events | `Collection.queried`, `File.generated` |

## Reference

| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `offer` | `Offer` | Required | The offer the kit describes. |
| `venues` | `VenueId[]` | `['product-hunt', 'hacker-news', 'reddit', 'x', 'linkedin', 'betalist', 'indie-hackers', 'peerlist-launchpad']` | The launch sites to compile for. |
| `kind` | `'launch-kit'` | `'launch-kit'` | The kit to compile. A listing kit for directories uses the same fields. |
| `maxCost` | `number` | `0.25` | 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 `Content.compiled`. Refuses with [`max-cost`](/code/refusals#max-cost) before it spends, and with the Data calls and Accounts groups ([refusals](/code/refusals)).