# Manage bundled format blueprints

> Ship output formats as committed data. Covers the `.gadget` archive plus `.json` sidecar split, `FORMAT_BLUEPRINTS_DIR` for forks, generation into the gitignored `src/generated/format-blueprints.ts`, first-request installation into KV and R2, `pnpm import:format-blueprint` with `--new`, and why a deployed `blueprintId` must never be renamed.

- Repository: cloudflare/cloudflare-os
- GitHub: https://github.com/cloudflare/cloudflare-os
- Human docs: https://grok-wiki.com/public/docs/cloudflare-cloudflare-os-838773bb92dd
- Complete Markdown: https://grok-wiki.com/public/docs/cloudflare-cloudflare-os-838773bb92dd/llms-full.txt

## Source Files

- `packages/workshop-backend/format-blueprints/README.md`
- `packages/workshop-backend/scripts/build-format-blueprints.mjs`
- `packages/workshop-backend/scripts/import-format-blueprint.mjs`
- `packages/workshop-backend/src/format-blueprints.ts`
- `packages/workshop-backend/src/admin-config.ts`
- `packages/workshop-backend/package.json`

---

---
title: "Manage bundled format blueprints"
description: "Ship output formats as committed data. Covers the `.gadget` archive plus `.json` sidecar split, `FORMAT_BLUEPRINTS_DIR` for forks, generation into the gitignored `src/generated/format-blueprints.ts`, first-request installation into KV and R2, `pnpm import:format-blueprint` with `--new`, and why a deployed `blueprintId` must never be renamed."
---

A *format* is an ordinary blueprint the deployment has promoted through `AdminConfig.formats`. The bundled set is the one it promotes out of the box: `packages/workshop-backend/format-blueprints/` holds a `<name>.gadget` archive plus a `<name>.json` sidecar per blueprint, committed as data. `scripts/build-format-blueprints.mjs` validates every sidecar and emits base64 archives into `src/generated/format-blueprints.ts`; `src/format-blueprints.ts` installs them into the `BLUEPRINTS` KV namespace and the `BLUEPRINT_CONTENT` R2 bucket on the first `/api` request a deployment serves. Nothing wakes on deploy — a fresh deployment is provisioned by its first visitor.

## The two-file split

| Holds | File | Rationale |
| --- | --- | --- |
| The code and the `bindings` it needs | `<name>.gadget` | What the blueprint *does* |
| `blueprintId`, `title`, `description`, `output` (`id`/`noun`/`plural`/`icon`), `author`, `revision` | `<name>.json` | What a human *curates*, kept as reviewable text rather than fields inside a binary |

`installOne()` spreads the archive's parsed metadata and then overwrites `title`, `description`, `author`, and `output` with the sidecar's values, so an archive's own title and author are inert. The import script normalizes the archive's metadata anyway so the committed bytes don't contradict the sidecar.

```text
format-blueprints/                  src/generated/format-blueprints.ts (gitignored, generated)
├── README.md                       ┌─────────────────────────────────────┐
├── workspace-docs.gadget  ──────►  │ FORMAT_BLUEPRINTS: [                │
├── workspace-docs.json    ──────►  │   { blueprintId, title, description,│
├── <name>.gadget                   │     output, author, revision,       │
└── <name>.json                     │     archive /* base64 */ }          │
                                    │ ] : BundledFormatBlueprint[]        │
                                    └─────────────────────────────────────┘
```

### Sidecar schema

The build validates the sidecar rather than the runtime, so a typo fails the build of whoever made it. Unknown top-level keys, unknown `output` keys, and unknown `author` keys are all rejected — silently ignoring one looks exactly like the field not working. `$comment` is accepted and dropped.

<ParamField body="blueprintId" type="string" required>
The install key. Must match `^[a-zA-Z0-9._-]+$` and must not be a reserved blueprint key (`.featured`, `.adminConfig`). Two sidecars in one directory may not share an id.
</ParamField>

<ParamField body="title" type="string" required>
Non-empty. Written over the archive's title at install time.
</ParamField>

<ParamField body="description" type="string" required>
Non-empty.
</ParamField>

<ParamField body="output.id" type="string" required>
Grouping key on the Outputs page. Keep it generic (`document`, not `acme-brief`) so a "Contract" blueprint lists with the other documents instead of adding its own filter chip.
</ParamField>

<ParamField body="output.noun" type="string" required>
Singular noun, e.g. `Document`.
</ParamField>

<ParamField body="output.plural" type="string" required>
Plural noun, e.g. `Documents`.
</ParamField>

<ParamField body="output.icon" type="string" required>
One of `fileText`, `gridNine`, `presentation`, `appWindow`, `flowArrow`, `kanban`, `chartBar`, `table`, `notebook`, `listChecks`. The list is duplicated in the build script because it runs before (and without) a TypeScript build; the runtime validates against the real `OUTPUT_ICONS`, so drift costs a build that rejects an icon the Worker would have accepted.
</ParamField>

<ParamField body="author" type="object" required>
`{ type?: "user", name: string, id: string }`. `type` must be `"user"` when present.
</ParamField>

<ParamField body="revision" type="integer" required>
Positive integer. Bumped when the archive bytes change, to trigger a reinstall on deployments already holding an older copy.
</ParamField>

## Generation

`node scripts/build-format-blueprints.mjs` runs as part of `build`, `types:check`, `test`, `test:integration`, and `test:watch` in `@gadgets/workshop-backend`, and standalone as `pnpm build:format-blueprints`. It reads every `*.gadget` in the source directory (sorted), requires a matching `<name>.json`, and writes `src/generated/format-blueprints.ts` — a generated module, so it is not committed and a clean checkout must run a build before `tsc` succeeds.

```
Bundled 3 format blueprint(s) from /…/format-blueprints, 71 KiB raw -> /…/src/generated/format-blueprints.ts
```

<Warning>
A `.gadget` with no matching `.json` is a hard error (`<file> has no <name>.json describing it.`). An **empty** directory is only a warning — `No *.gadget archives in <dir>; the deployment will bundle no formats.` — because shipping no formats is supported. A mistyped `FORMAT_BLUEPRINTS_DIR` fails in `readdir()`, which is the case worth catching.
</Warning>

## Installation on first request

```mermaid
flowchart LR
  subgraph src["Build-time source"]
    dir["format-blueprints/<br/>&lt;name&gt;.gadget + &lt;name&gt;.json"]
    build["scripts/build-format-blueprints.mjs"]
    gen["src/generated/format-blueprints.ts<br/>FORMAT_BLUEPRINTS (base64)"]
  end
  subgraph worker["Worker runtime"]
    install["src/format-blueprints.ts<br/>installFormatBlueprints()"]
    parse["parseBlueprintArchive()<br/>(blueprint-archive.ts)"]
  end
  subgraph store["Storage"]
    r2[("BLUEPRINT_CONTENT R2<br/>&lt;blueprintId&gt;/&lt;version&gt;")]
    kv[("BLUEPRINTS KV<br/>&lt;blueprintId&gt; → BlueprintKvRecord")]
  end
  dir --> build --> gen --> install
  install --> parse
  install -->|"1. content"| r2
  install -->|"2. metadata"| kv
  install -->|"BlueprintPublicInfo[]"| featured["featured mirror"]
```

`installOne()` parses each base64 archive through the ordinary `parseBlueprintArchive()` reader, so a corrupt bundled file fails exactly as an uploaded one would instead of producing a half-installed blueprint. It buffers the content (R2 needs a known length, and the archive is already fully in memory from the Worker bundle) and throws if the declared `contentLength` disagrees with the bytes held. The archive's content section is already gzip-compressed, which is what R2 stores.

Write order is content first: metadata without its R2 object is a broken blueprint, while the reverse is only an orphaned object the next install overwrites.

- `BLUEPRINT_CONTENT.put(`${blueprintId}/${installed.version}`, contentBytes)`
- `BLUEPRINTS.put(blueprintId, JSON.stringify({metadata: installed}))`

`installFormatBlueprints(env)` loops the whole set, logs `formats.install.ok` per success and `formats.install.failed` per failure through the `workshop.formats` logger, and returns the `BlueprintPublicInfo[]` that installed. One bad archive must not deny the deployment the others; failure is tolerable, since a deployment with none installed simply has no standard formats.

<Info>
Installation writes an ordinary blueprint — metadata into `BLUEPRINTS`, the code snapshot into `BLUEPRINT_CONTENT` — exactly as publishing does. There is no reserved id prefix and no fallback branch in the read path, so nothing downstream knows these are special.
</Info>

### Reinstall fingerprint

`formatBlueprintsManifestVersion()` builds the identity of the installed set:

```ts
// packages/workshop-backend/src/format-blueprints.ts
return FORMAT_BLUEPRINTS
    .map(e => `${e.blueprintId}@${e.revision}+` +
        fingerprint(JSON.stringify([e.title, e.description, e.author, e.output])))
    .toSorted()
    .join(",");
```

Everything that ends up in the installed metadata contributes, not just `revision` — editing a description would otherwise build, deploy, and change nothing on a deployment that had already installed. `revision` covers the one input the fingerprint cannot see: the archive bytes. `fingerprint()` (from `src/admin-config.ts`) is FNV-1a as eight hex characters, compared only for equality.

## Changing a title, description or author

Edit the sidecar and rebuild. No archive rewrite and no `revision` bump: those fields are part of the fingerprint, so a reinstall follows on the next deploy.

## Updating a blueprint's code

<Steps>
<Step title="Build and export in a real Workshop">
Iterate on the blueprint in a running deployment and export it as a `.gadget` archive.
</Step>
<Step title="Import the export by blueprintId">
```bash
pnpm import:format-blueprint ~/Downloads/Gadgets-Doc-v4.gadget format.document
```

The script resolves the id against every sidecar in the directory and writes to that pair's `.gadget` and `.json`. Nothing is written before the incoming archive is read and parsed, so a bad argument leaves the repo untouched. Invoked with no usable arguments it exits `2` and lists the formats it found:

```
usage: pnpm import:format-blueprint <export.gadget> <blueprintId>
       pnpm import:format-blueprint <export.gadget> --new <name>

formats in /…/format-blueprints:
  format.document      workspace-docs.gadget
```
</Step>
<Step title="Read the report">
```
Updated workspace-docs.gadget (format.document)
  code         23668 -> 24489 bytes (7c5413e5a482)
  bindings     (none)
  version      3 -> 4
  revision     2 -> 3  (workspace-docs.json)

  presented as "Workspace Docs" by Cloudflare, from workspace-docs.json
               [export called it "Gadgets Doc"]
```

The line worth reading is **`bindings`**, flagged `[CHANGED]` when the export needs something the old copy didn't — an instantiating user will now be asked for it.
</Step>
<Step title="Commit both files">
The archive rewrite, the bumped `revision`, and the regenerated `src/generated/format-blueprints.ts` all land together. `revision` is automated because forgetting it is invisible: everything builds and deploys, and the old blueprint quietly stays put.
</Step>
</Steps>

The importer re-implements the archive format because it runs as a plain Node script outside the Worker — `src/blueprint-archive.ts` is the authority. A 24-byte prefix carries magic `0xec2e2d3a2300e317`, `VERSION` 1, the metadata byte length, and the content byte length, followed by UTF-8 JSON metadata and a gzipped Yjs snapshot. `parseArchive()` rejects a short file, bad magic, an unsupported version, a content length that disagrees with the prefix, and metadata that is not valid JSON. After writing, the script round-trips the bytes it just wrote and re-checks the metadata and a content hash, because these are committed as data and a corrupt archive would otherwise first surface when a deployment tried to install it.

## Adding a new format

`--new` writes the sidecar for you, filling in what it can from the export:

```bash
pnpm import:format-blueprint ~/Downloads/Brief.gadget --new acme-brief
```

Scaffolded values:

| Field | Source |
| --- | --- |
| `blueprintId` | the `--new` name |
| `title` | `incoming.metadata.title`, else the name |
| `description` | `incoming.metadata.description`, else `TODO: say what a <title> is for.` |
| `output` | `{ id: <name>, noun: <title>, plural: "<title>s", icon: "appWindow" }` |
| `author` | the first sibling sidecar's `author`, else the export's `author` |
| `revision` | `1` |

The scaffolded sidecar is held rather than written until the archive proves usable. `--new <name>` must match `^[a-zA-Z0-9._-]+$` and must not collide with an existing sidecar name — import into an existing one by `blueprintId` instead. The script then prints the fields worth editing before deploy, chiefly `output` and especially `output.id`.

<Warning>
`blueprintId` defaults to the `--new` name and is the install key. Reimporting the same id updates that blueprint in place. **Changing it after a deployment has installed it** promotes the new id as a *second* format while the old one stays in the New menu, updated by nothing. Rename files freely; the id is the load-bearing part.
</Warning>

## Shipping your own formats

`packages/workshop-backend/format-blueprints/` is only the default. `FORMAT_BLUEPRINTS_DIR` (resolved relative to the package root) points both the build and the import script somewhere else:

```bash
FORMAT_BLUEPRINTS_DIR=../../acme-formats pnpm build
```

Whatever directory it names *is* the deployment's format set — it replaces the default rather than adding to it. Keep it in your own tree in the same `<name>.gadget` + `<name>.json` layout; nothing in it refers back to this repo. To keep one of the bundled formats, copy the pair across once and own it from then on.

This matters because this repo is usually a submodule: adding or deleting files in the default directory would conflict on every update, while pointing the build at your own directory touches nothing.

Two lighter options need no build change:

<AccordionGroup>
<Accordion title="Promote your own blueprints">
These are ordinary blueprints, and the standard set is admin curation (`AdminConfig.formats`). Publish a blueprint in your deployment and promote it in the admin Formats panel; disable the bundled ones you don't want. Nothing needs rebuilding — this is the mechanism the bundled set is a convenience on top of, not a special case beside it.
</Accordion>
<Accordion title="Ship no formats">
Point `FORMAT_BLUEPRINTS_DIR` at an empty directory. The build warns, and the deployment has no formats until an admin promotes something.
</Accordion>
</AccordionGroup>

## How a bundled blueprint becomes an offered format

`AdminConfig.formats` is a `FormatCuration[]`, and order is menu order:

<ResponseField name="blueprintId" type="string">
The promoted blueprint's id.
</ResponseField>

<ResponseField name="enabled" type="boolean">
Offered to users and the agent. Disabling keeps the entry and its overrides, so re-enabling doesn't lose the admin's edits. Parsing treats a missing value as enabled (`enabled !== false`).
</ResponseField>

<ResponseField name="agentHint" type="string">
One line telling the agent when to choose this format. Trimmed and truncated to `MAX_AGENT_HINT` (400) — every enabled format's hint goes into the system prompt on every turn, so this is a budget.
</ResponseField>

<ResponseField name="overrides" type="Partial<BlueprintOutput>">
Presentation the deployment substitutes for the blueprint's own, e.g. an org that calls its decks "Briefings". Absent fields fall back to the blueprint's declaration.
</ResponseField>

`parseFormats()` drops malformed entries and duplicate `blueprintId`s. `reorderFormats()` throws `Format order must list each promoted format exactly once.` unless the supplied id list is a permutation of what is promoted, so a stale client cannot silently drop a format. `defaultOutputFormatId()` supplies a stable grouping id for a promoted blueprint that declares no `output`, shortening ids longer than 40 characters to a 31-character prefix plus an eight-hex `fingerprint()`.

<Note>
The blueprint's own `BlueprintMetadata.output` is a declaration, not a promotion: any user can publish a blueprint calling itself a Document, but only `AdminConfig.formats` decides what the deployment offers.
</Note>

## Failure modes

| Symptom | Cause | Fix |
| --- | --- | --- |
| `<file> has no <name>.json describing it.` | Archive committed without a sidecar | Add the sidecar, or import with `--new` |
| `<name>.json: unknown keys: …` | Typo or an unsupported field | Remove it; the schema is closed |
| `<name>.json: output.icon must be one of: …` | Icon outside the build's `OUTPUT_ICONS` copy | Use a listed icon |
| `<a>.json and <b>.json share id <id>` | Two sidecars with one `blueprintId` | Two archives installing under one id would race and only one would survive — give them distinct ids |
| `<name>.json: blueprintId <id> is reserved` | Used `.featured` or `.adminConfig` | Pick another id |
| `no sidecar declares blueprintId "<id>"` | Wrong id, or a first import | Use the printed list, or `--new <name>` |
| `<name>.json already exists; import into it by blueprintId instead` | `--new` collided with an existing stem | Drop `--new` and pass the id |
| `<label>: not a .gadget archive (bad magic)` / `unsupported archive version N` / `content is N bytes, prefix claims M` | Truncated, wrong-format, or corrupt export | Re-export from the Workshop |
| `formats.install.failed` in Worker logs | A bundled archive failed to parse or install | The others still install; re-import the offending archive and redeploy |
| Missing `./generated/format-blueprints.js` on a clean checkout | Generated module not yet built | Run `pnpm build:format-blueprints` (or any `build`/`types:check`/`test` script) |
| New description deployed but the deployment shows the old one | Fingerprint unchanged, so no reinstall | Confirm the sidecar edit landed in the generated module; archive-only changes need a `revision` bump, which the importer does |

## Related pages

<CardGroup cols={2}>
<Card title="Blueprints" href="/blueprints">
What a blueprint captures, the three binding types, `.gadget` export/import, and KV propagation.
</Card>
<Card title="Admin configuration reference" href="/admin-configuration">
The `AdminConfig` schema, its defaults, and the reserved `.adminConfig` KV key.
</Card>
<Card title="Routing and worker bindings" href="/routing-and-bindings">
Where `BLUEPRINTS` and `BLUEPRINT_CONTENT` are bound, and how `/api/*` reaches the backend.
</Card>
<Card title="Build, lint, and test" href="/build-lint-test">
Generator prerequisites and the recursive `build` / `types:check` ordering CI enforces.
</Card>
<Card title="Quickstart" href="/quickstart">
First prompts that exercise bundled format blueprints versus from-scratch gadget creation.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Missing generated modules on a clean checkout and other known failure modes.
</Card>
</CardGroup>
