Reference

Every plugin and bb command, grouped by job, with its flags. Open More under a command for the details, the edge cases and the why.

Quick index

Each command's line, usage and flags come from bb help --json when the site is built, from bb 0.23.0.

Plugin commands

You run these in Claude Code. Each loads a skill, and the agent follows it. Getting started walks through them in order.

Command What it does
/backbase:new <figma frame link> [folder] [--space <id>] Starts from nothing: checks the machine, makes the project in a new folder, plans the frame and stops at Gate 1.
/backbase:analyze <figma frame link> [--project <dir>] In a project you have: reads the frame, plans every part against the catalog and stops at Gate 1.
/backbase:sync [--project <dir>] Gate 2: plans the write of components, datasources and the frame’s theme to the space, shows every change, and writes after your yes.
/backbase:compose [--project <dir>] [--slug <slug>] Builds the approved plan as a story, checks it, reviews it beside the frame, and pushes it as a draft after your yes.
/backbase:edit [<story id or full path>] <what to change> [--project <dir>] Changes a story already in your space, only the sections you ask for, and pushes it as a draft after your yes.
/backbase:refine [slug] [--project <dir>] [--tries <n>] [--budget <n>] [--fit-drop <fit>] Measures the page against the frame, keeps what gets closer, undoes the rest, and pushes once after your yes. See Refining the page.
/backbase:component [<story-file>] [--project <dir>] Builds the Wrap and Create components the page’s slots wait for. See the component stage.
/backbase:upstream [<component> | requests] [--project <dir>] Takes a component to Backpack (an issue, then a draft pull request), or files design requests to GCS, each after your yes.
What each one writes
  • /backbase:new and /backbase:analyze: only the project, bb create’s files and .backbase/. Nothing in Storyblok or Figma.
  • /backbase:sync: only the space plan you approve.
  • /backbase:compose: the images you approve, and one story, as a draft, after you approve its push plan.
  • /backbase:edit: the images in the image plan you approve, and the story it pulled, after you approve its push plan. It never overwrites a section someone changed in Storyblok since the pull unless you name it.
  • /backbase:refine: the local story and its ops journal while it iterates, and .env.local with your yes, restored at the end. The story goes to Storyblok once, after you approve its push plan.
  • /backbase:component: local component code, the components-only Gate 2 plan you approve, and one draft after its push plan.
  • /backbase:upstream: drafts in .backbase/upstream/ and .backbase/requests/, and on GitHub only the issue, the draft pull request and the request issues and comments you approve, each on its own yes.

Nothing publishes, and nothing writes to Figma. /backbase:refine --sections <n> still works for now, deprecated: it’s read as a budget of that many sections’ tries.

How bb runs

bb is the Backbase CLI, published to GitHub Packages as @ef-global/backbase. The plugin runs your project’s own bb from node_modules/.bin, or else npx -y @ef-global/backbase@<version>, pinned to the version the plugin was released with. Both are released together.

  • --json prints one envelope: { ok, command, data, errors }.
  • Exit codes: 0 when it succeeds, 1 when it fails, 2 on a usage error.
  • bb masks secrets from your environment in everything it prints.

Flags for every command

FlagWhat it does
--jsonPrint one JSON envelope instead of text
-h, --helpPrint help
-v, --versionPrint bb's version

Start

bb doctor

Check that this machine, the project and its Storyblok space are ready for Backbase, and say how to fix what isn't.

bb doctor
FlagWhat it does
--fixApply the fixes bb can make itself, then list the manual steps left
--writeAdd the Chaperone preset extends and check the project is ready for a write to Storyblok: its token, project and space checks must pass, and a warning or a skip fails
--space <space>The project's Storyblok space id, over spaceId in storyblok.config and space.id in .backbase/config.json
More

Checks run in three groups: the machine’s, the project’s and the space’s. Getting started says what each checks, and Troubleshooting gives each fix. Plain bb doctor fails only on a failure.

--write is the check before anything writes to Storyblok. Run it in the project. These must pass, and a warning or a skip counts as a failure:

  • storyblok-token, or project-storyblok-token when the token is in the project’s .env
  • project-backpack, project-storyblok-token and project-env-ignored. A shell token that overrides a different one in .env only warns, and doesn’t block: space-access checks the token sb-mig will use
  • space-id and space-access

It ends with Ready to write to Storyblok., or the checks that aren’t ready. Its exit code follows that alone: 0 ready, 1 not. Other failures, such as github, still show but don’t fail the run. Each blocker is an error, check-failed or not-ready-to-write, and data.write is { ready, blockers }. --write and --fix also set the project up for Chaperone and the slot component (see the Chaperone preset).

Notes on single checks:

  • figma-mcp is left out under --write. Plain doctor says Figma: checked when the frame is read: it passes with a local connector or plugin, and skips neutrally otherwise. A local MCP that needs sign-in doesn’t warn: your claude.ai connector can still work.
  • With the token in .env, storyblok-token passes with Covered by the project's .env (STORYBLOK_OAUTH_TOKEN).
  • Three advisory checks never block a write, in plain doctor or --write:
Check What it reports
project-translation-strategy NEXT_PUBLIC_TRANSLATION_STRATEGY, read as Next does: shell, .env.local, .env, else none. none and field use root stories. folder warns: the site asks for <locale>/<slug>, and compose writes under the default locale’s root folder. An unsupported value warns.
space-settings-story A settings story of content type settings: at the root, or in every locale folder for folder (locales from languageMap.locales in src/lib/i18n/languageMap.mjs). Missing, the wrong type or unreadable warns. Doctor creates nothing.
space-field-plugins Each field plugin the project’s components use. It warns only when the account’s or organisation’s list assigns the plugin to other spaces and not this one, with the fix: install it, or bb space plan --exclude <component>. A plugin it can’t verify is a note; an unreadable catalog warns.

The space and the token:

  • The space id comes from --space, then spaceId in storyblok.config, space.id in .backbase/config.json, STORYBLOK_SPACE_ID in your shell, then in the project’s .env. An empty source falls through; an invalid value fails. The catalog, the asset commands and story validation use the same order, without --space.
  • Doctor evaluates storyblok.config as bb catalog does. Like sb-mig, it loads .env first without overriding your shell.
  • It asks the Management API sb-mig uses: storyblokApiUrl in storyblok.config, then NEXT_PUBLIC_STORYBLOK_MANAGEMENT_API_URL, then the EU API. space-access gives Storyblok 10 seconds to answer.
  • The token goes only over https, and only to Storyblok’s Management API: EU https://mapi.storyblok.com/v1, US https://api-us.storyblok.com/v1, CA https://api-ca.storyblok.com/v1, AP https://api-ap.storyblok.com/v1, CN https://app.storyblokchina.cn/v1 (and localhost or 127.0.0.1, for tests). Any other host, or plain http, fails space-access and the token isn’t sent. sb-mig’s own list has no Canada.
  • Doctor never prints a token: not in the table, the JSON or the errors.

bb create

Make a new site from the boilerplate: clone it, install, add Backbase's files and commit.

bb create <dir>
FlagWhat it does
--space <space>The Storyblok space id, recorded as a sandbox in .backbase/config.json and written to .env
--ref <ref>The boilerplate branch or tag to start from (default master)
--transport <transport>How to clone: auto (SSH, then HTTPS; the default), ssh or https
More

Nothing is written to Storyblok.

It runs its steps in order and stops at the first that fails, with the step’s message and its fix. It checks the folder first, then the machine checks it needs (node and git), before it writes anything. It never overwrites a file. Troubleshooting has each way it stops.

bb version

Print bb's version.

bb version

bb help

Print help, for bb or for one command.

bb help [command...]

bb droid

Roll bb's droid across the terminal.

bb droid

Plan

bb catalog

List this project's Backpack and local components, placements, presets and problems.

bb catalog
FlagWhat it does
--component <component>Show one component by its schema name
--knowledgeCompile the knowledge pack instead: one compact line per blok the root can hold, with its fields, design subfields, placements, parents and presets, and the colour names from the project's datasources
--root <root>With --knowledge, the story's root blok to scope it to. Default: page
--out <out>The file to write it to instead of printing it: the catalog's --json envelope, such as .backbase/catalog.json, or with --knowledge the pack, such as .backbase/knowledge.md
--if-staleWith --out, write it only when the file there was compiled from another Backpack, other schemas or another bb, and say whether it did. A pack's --root and --sections aren't compared
--sections <sections>With --knowledge, the frame's sections.json: its colour variables (design.tokens.color) are mapped to the colour names a picker stores
--space <space>The project's Storyblok space id, over spaceId in storyblok.config and space.id in .backbase/config.json
--space-presets <space-presets>A space-presets.json, to add the Storyblok space's presets to the catalog's
More

Use --component <blok> to see one blok, including what only Backpack’s React has, such as React only: CardFooter.variant: testimonial: a lead to check, not a fact, since a wrapper may map its own option names. data.stalePresets lists presets that name bloks Backpack 4 doesn’t have.

The knowledge pack

bb catalog --knowledge --out .backbase/knowledge.md compiles the catalog into the compact text the agent reads to choose bloks and presets and write design values, one line per blok:

- sb-tag | fields: label, icon:asset-selector(icons), aria_label | design: $d2, $d202, $d185, $d99, $d16, $d13, $d1 | placement: $p11 | parents: sb-card-header,sb-content-card,sb-flex-group,…
  • What many bloks share is said once: $o option sets, $c sets of bloks, $d design subfields, $p subfields a blok has only under some parents. A colour picker lists its datasource’s real names. A preset manifest ends it, with the space’s presets too under --space-presets.
  • --sections adds a colour map: each semantic variable the frame uses, and the colors entry its theme points at (color-system-surface-brand-accent-3 → Color/Red/Lightest (100)). Where the frame paints it another colour, the line ends ≠ frame #hex, then | write <name> when a colors entry has that hex, or | ≈ approximation, which the compose report lists. It never guesses.
  • <house_style> holds how Backpack’s own presets are built: rules that hold at least 90% of the time over at least five bloks, with their counts, such as (9/9), and one hand-ruled hero rule marked CURATED. Compose follows the frame where it states a layout, and the house style where it’s silent.
  • <section_grid> and <flex_alignment> come from the render table: what a position’s numbers are at each breakpoint (in 4.11.0, lines 0–6 at s and m, 0–16 from l, full bleed [0,6] and [0,16]), and which field moves a flex box’s children which way (in sb-flex-group, align is the vertical one).
  • It’s scoped to what the root blok holds, page unless --root, and stays under 62,500 characters for an ILS-sized project.

When the catalog goes stale

The catalog and the pack record what they were compiled from: Backpack’s version, a hash of every file bb catalog read (Backpack’s schemas and what they load, storyblok.config, your schemas and datasources and their imports, component-list.ts, component-map.exceptions.json, backbase.placements.json, .backbase/render-semantics.json and the space-presets file), and the bb that compiled them. Line endings don’t count.

  • Every command that takes --catalog refuses a catalog saved for another Backpack, or before a schema changed: catalog-stale, naming the versions or the files, and ending with the command that rebuilds it. One from another bb is still used when its Backpack matches.
  • --out writes through a temporary file, so nothing reads half a catalog. --if-stale rebuilds only when the fingerprint differs, and says why in data.rebuilt and data.why.
  • The skills rebuild both before they use them, and tell you in one line. bb doctor’s catalog check warns about a stale catalog.

bb catalog render-semantics

Render every Backpack blok and design value with the project's own wrappers, on the server, and write what each renders as in CSS and px: the field → CSS table bb catalog shows as renderSemantics. --measure checks it against the computed style of the probes' story in the project's page.

bb catalog render-semantics
FlagWhat it does
--out <out>The file to write, .backbase/render-semantics.json when unset: bb catalog reads it from there
--component <component>Render only this blok's probes
--story <story>Also write the probes as one story to this file, for bb story serve to serve and --measure to read, such as .backbase/stories/backbase-render-semantics.json
--measureMeasure the probes' story in the page at --url with Playwright, and record where it disagrees with the static table and what only the page shows
--url <url>With --measure, the page that renders the probes' story, such as http://localhost:3000/backbase-render-semantics
--width <width>With --measure, the viewport width in px, 1440 when unset
More

A design value’s label isn’t its px on the page. In Backpack 4.11.0, item_spacing: s pads each list row 16px above and below, so rows sit 32px apart; item_flex: auto is Fill; sb-content-group puts no gap between its children; a card body pads 24px. bb’s render table says what each blok and value renders, in CSS and px, on which element. bb catalog shows it as renderSemantics, bb story design get under each subfield, and the knowledge pack in <field_spacing>. Compose and refine match the frame’s px to what a value renders, not to its label.

bb ships the table for 4.11.0. This command builds it from the project’s own Backpack, rendering with its wrappers on your machine, and bb catalog uses it from then on. Nothing is fetched or written to Storyblok. A table for another Backpack is ignored with a warning, and with none at all, the catalog, the pack and design get say so.

bb figma sections validate

Check a sections.json against the sections contract (v1).

bb figma sections validate <file>
More

It checks the file against the sections contract.

bb figma sections design

Write the design script's read into a sections.json: each section's layout, tokens, instances, icons and images.

bb figma sections design <file>
FlagWhat it does
--raw <raw>The design script's output, if it isn't figma-design.raw.json next to the sections.json
More

The read skill runs a read-only script through your Figma connector for each part, and saves its output as figma-design.raw.json beside sections.json. This turns it into each section’s design and copy, offline. Both are optional.

Field What’s in it
layout The auto-layout tree: each layer’s mode, gap, padding, alignment, sizing, fill, stroke, radius, effects and text styles, with the variables they’re bound to. Inside a library component, only what holds text, an image or a component, without its look.
tokens The variables the section uses, by kind, and its styles.
instances Each component, with the variant and boolean properties its instances use.
icons The icons from “EF Backpack Icons”, by name.
images Every visible image fill: layer, hash, size and scale mode.

copy is every visible text layer, in order, as { "id", "lines" }, nothing cut. Inside an instance, a node id starts with the instances it sits in (I2058:22115;3412:9002), which is how bb story fill tells one card from the next.

A large part is read in pages, and this puts them back together, up to 600 layers a section. When a section still isn’t whole, truncated is true and the agent tells you. The read never writes to Figma: a test checks the script for every Plugin API call that could.

bb figma images match

Match image downloads by hash or exact name and VIDEO posters by node id; write images.json.

bb figma images match <sections>
FlagWhat it does
--downloads <downloads>Folder containing Figma download_assets files and node-named VIDEO poster PNGs
More

The read skill downloads the original image fills with your Figma MCP’s download_assets. This matches the files by their bytes first, then by hash, node id or layer name, writes images.json beside sections.json, and lists what it couldn’t match in data.unmatched. A video is exported as a PNG poster of its node alone, without the player’s controls, and matched by node id, with note: "poster of a video".

bb plan validate

Check a plan.json against the plan contract (v1) and its sections.json.

bb plan validate <plan>
FlagWhat it does
--sections <sections>The sections.json the plan was made from
--space <space>The project's Storyblok space id, over spaceId in storyblok.config and space.id in .backbase/config.json
--space-presets <space-presets>A space-presets.json, to add the Storyblok space's presets to the catalog's
--datasources <datasources>The space's colour datasources, as bb space datasources writes them, if not .backbase/datasources.json
--write-design-fitStore each section's design fit in the plan, when it passes: what its bloks' design options can't express
More

It checks the plan contract, and warns when a blok lacks a required child, or the design fit is missing (design-fit-missing) or stale (design-fit-stale). A structure that selects a stale preset fails. A warning or a gap never fails the plan.

Design fit says whether the planned bloks’ design options can make each part look like the frame. For each entry with a design read, it pairs the Figma layers with the planned bloks in reading order and judges each fact: layout, spacing, colours, radii, strokes, shadows, overlays, text styles and variants.

  • exact: an option expresses it.
  • approximate: the nearest option stands in, such as a 3px gap as xxs (4px). A spacing, gap or radius off the scale is never written silently: it records the px, the field and the step (offScale), and each is a request to GCS. Halfway between two steps, padding and margins take the step Backpack’s presets use more (the ILS 96px padding is xxxl, 128), and gaps and radii the smaller. A colour takes the nearest entry by ΔE00 only within 10.
  • gap: nothing can express it, such as a gradient border or an overlay on an image. Each is a request for the Backpack team.

A section’s fit is its worst fact’s. Colours are judged against .backbase/datasources.json, or --datasources, else the catalog’s datasources, alpha included: #191919 at 65% matches rgba(25, 25, 25, 0.65).

--write-design-fit stores each fit in the entry’s designFit once the plan passes: up to ten facts, gaps first, with the request evidence in requestFacts. A failing plan stores nothing (design-fit-not-stored).

Full bleed. A layer bleeds when its section starts at the frame’s left edge and it is as wide as the frame. Its blok goes on the section grid’s bleed lines, [0,6] at s and m and [0,16] at l in 4.11.0, and an sb-image takes viewport_size 100vw, at the frame’s breakpoint and below only. bb stores it in designFit.bleed, and compose writes it as it is.

bb plan show

Print a plan.json as the markdown table a person reviews at Gate 1.

bb plan show <plan>
FlagWhat it does
--fullShow each section's whole structure, not just 2 levels below its root
--questionsList each section's questions under the table
--approximationsList each section's other design approximations, not just how many there are
More

With a stored design fit, the table gets a Design fit column (fits, ≈ 3, gap: stroke, overlay · ≈ 2, or — when not judged), and under it, per section: the design gaps, what another blok would fit, how many approximations (--approximations lists them), what wasn’t judged, the off-scale values (Padding: 96px, with every section and field) and what runs full bleed. Then the planned slots. The same lists are in .backbase/reports/gate-1.md.

Open questions. Confirming the plan never answers a question. Each is { "question", "recommended", "answer" }, the answer null until you give it. plan show marks each Recommended: … or Answered: …, and while any is open ends with a Confirmation that names each recommended answer: Confirm, with: the image bleeds. A yes to the Gate 1 card answers only the questions it showed, with the answers it showed. A question with no recommended answer needs your own words before the plan is confirmed: the agent never picks one for you.

bb plan requests

Draft design-gap, off-scale, palette and stale-preset requests from the Gate 1 plan, for /backbase:upstream to file after a person's yes.

bb plan requests [plan]
More

After you confirm the plan, the analyse flow runs it and offers .backbase/requests/design-gaps.md. It works offline, from the plan, the frame’s sections.json and the catalog; only the icon manifest is fetched, once. One draft for each:

  • capability a blok lacks, across sections, with the Figma evidence;
  • value off a scale, per capability and px (Padding: 96px), with every section and field: ILS’s 96px padding is one request, not eleven;
  • palette mismatch, for design and Backpack together. Palette drafts carry a TODO to recheck theme overrides before filing;
  • icon that icons/bold doesn’t have (Icon: google), with the closest names. A global section’s icons ask for nothing;
  • set of stale presets, with each file, component and unknown name.

Each sits under its key’s marker (<!-- backbase-request: padding-96px -->). Wrap and missing-primitive requests stay in plan.json. It rewrites only design-gaps.md, and keeps each Filed: link. Refresh an older plan first with bb plan validate <plan> --sections <file> --write-design-fit; it warns when you need to.

The requests go to GCS only through /backbase:upstream requests, after your yes. The analyse flow never files one, and filing blocks neither the plan nor Gate 2.

Sync

bb space plan

Plan the write of the project's components and datasources to its Storyblok space, and with --sections the frame's palette as its theme, without writing: the plan a person approves at Gate 2.

bb space plan
FlagWhat it does
--sections <sections>The frame's sections.json: plan its palette as the space's theme, the user values of colors-theme-light and the custom-colors entries. bb space sync takes the same one
--components-onlyPlan or sync components only, without datasources or a theme (for the component stage)
--exclude <exclude>Leave a component out of the plan, as name or name=reason, the reason the person gave: such as --exclude "sb-submit-button=the space lacks the description plugin". Repeat it for more. The plan lists it, the fingerprint covers it, and bb space sync takes the same --exclude
More

It writes nothing to the space. In order, it:

  1. checks what bb doctor --write checks, and stops when the project isn’t ready;
  2. checks the project’s sb-mig is 6.5.0-beta.25 or later (the boilerplate pins one that is);
  3. runs sb-mig sync datasources --all --dry-run --json, then sb-mig sync components --all --presets --dry-run --json, with nothing on stdin and the checked space as STORYBLOK_SPACE_ID;
  4. with --sections, plans the theme;
  5. saves .backbase/space-plan.json and prints what it would create and update, what’s left out, the presets, the theme, the missing field plugins, every destructive change on its own line, the fingerprint and the exact bb space sync command.
  • Destructive changes can lose content: field-removed, likely-rename, type-changed and whitelist-narrowed. The fingerprint hashes the space and every planned change, presets included.
  • The approval is the plan’s exact approvalSentence, which names the fingerprint and every destructive change. A generic yes never approves a destructive change.
  • Presets. sb-mig writes every preset by name on every sync, overwriting edits made in Storyblok, so the plan lists them by component. Stale presets are listed with their files, and approving the plan still writes them: the sentence says how many.
  • --exclude drops a component and its destructive changes, and lists it in plans.excluded with your reason. The fingerprint covers the names, not the reasons. An unknown name is exclude-unknown.
  • Field plugins. Each plugin a component’s top-level custom fields use is available, missing or unverified. It’s missing only when every listing of it assigns it to other spaces. A 401, 403 or 404 on a list leaves it unverified, never missing. Each missing one is in missingFieldPlugins, with the way round: install it, or --exclude the component.
  • --components-only plans only the components, for the component stage, and its fingerprint binds that scope. It can’t go with --sections.

The theme part

Site Builder’s theme is the Settings story’s backpack-theme field, rebuilt from the colors-theme-light and colors-theme-dark datasources, whose user dimension holds the space’s overrides. So bb writes user values, then saves the Settings story’s field as a draft, rebuilt as the plugin rebuilds it (ADR 0031).

With --sections .backbase/figma/<node-id>/sections.json, for each semantic colour the frame paints with, it compares what the space paints today:

  • set when the token has no user value, change when it has another, reset when the default is already the frame’s colour.
  • The value is var(--color-<primitive>) for an exact colors match, else the hex, such as #E0D7F9. It never snaps to the nearest.
  • Each colour of the frame’s own is offered as a custom-colors entry, such as Package (ILS) · System/Surface/Brand/Accent/3. The datasource is created when the space has none.
  • Light theme only: dark keeps Backpack’s defaults, and a note says so. A translucent paint isn’t planned.
  • A first sync plans no theme: plan again after it.
  • A user value it replaces is lost from the space, so it’s named in the approvalSentence, listed with the destructive changes and named in the hook’s ask. .backbase/space-sync.json records each previous value.
  • For each Settings story (settings, or <locale>/settings), the plan shows each token whose selected value changes. A missing one is a note, never an error.
  • The fingerprint covers the theme’s writes, the values they replace and when each Settings draft was saved, so a change in the space after the plan makes bb space sync refuse.

bb space sync

Write the plan a person approved at Gate 2 to the Storyblok space: plan again, and sync only when the fingerprint still matches, then write its theme part, whether or not a component failed.

bb space sync
FlagWhat it does
--components-onlyPlan or sync components only, without datasources or a theme (for the component stage)
--exclude <exclude>Leave a component out, as bb space plan --exclude had it: the same names. bb refuses when they differ from the approved plan's
--approved <approved>The fingerprint of the plan the person approved, as bb space plan printed it. bb refuses when the plan has changed since
--allow-destructiveAllow the plan's destructive changes. Only when the person approved each of them by name
--sections <sections>The frame's sections.json: plan its palette as the space's theme, the user values of colors-theme-light and the custom-colors entries. bb space sync takes the same one
More

Never --ssot.

It plans again, and refuses, exit 1, writing nothing, when:

  • the fingerprint isn’t the approved one (plan-changed): plan again;
  • the plan has destructive changes and no --allow-destructive (destructive-not-allowed). The agent passes it only after you approve each by name.

Then it runs sb-mig sync datasources --all --json and sb-mig sync components --all --presets --json, or names the components when some are excluded, and saves both reports in .backbase/space-sync.json.

  • Errors, exit 1: component-failed for each component sb-mig couldn’t write, with --exclude as the way round (a missing field plugin’s 422 names the plugin, and fieldPluginFailures lists it); component-not-synced when sb-mig’s report lacks a component it was given; sync-failed for a whole step. When the datasources fail, the components don’t run. Nothing is rolled back.
  • The theme is written last, even when a component failed. It’s skipped, theme-plan-again, when the sync wrote a datasource it was planned from, or someone changed a token or saved Settings since the plan. Only colour values are sent, and defaults never change. A failed write is theme-write-failed, in data.theme.
  • Settings is saved with only its theme field changed, as a draft, never published. space-sync.json records previous and previousModel, to restore by hand.
  • Storyblok has no conditional write, so the last save wins: a Settings tab opened before the sync holds the old theme, and saving it brings it back. Close Settings before the sync, reopen it after, and publish it yourself when you’re happy.

Safety. bb never runs sb-mig with --ssot, which deletes every component the code doesn’t have, and the content in them. No flag asks for it. The plugin’s hook blocks any command where sb-mig is followed by --ssot, however it’s run (npx, pnpm exec, dotenv --, find -exec, bash -c, a heredoc fed to a shell, npm run <script> -- --ssot or pnpm <script> --ssot), and lets text-only commands such as grep or git commit -m through. A package script with --ssot written into it is out of its reach, so the agent never runs the project’s sync scripts. The hook also blocks bb space sync without the plan’s fingerprint, or with --approved given twice, and makes Claude Code ask you first, naming the space and what it writes. The agent can’t answer that for you.

bb space datasources

Read the project's Backpack colour datasources from its Storyblok space into .backbase/datasources.json, read-only: the entries bb story materialize stores a colour name as.

bb space datasources
More

A colour picker stores the entry an author picks, with its id, and the ids are the space’s. This reads colors, custom-colors, colors-theme-light and colors-theme-dark, every page, as the picker reads them, read-only, through the same guarded client as bb doctor. It fails when the space has no colors: sync the datasources first. Run it after a sync. The file is a snapshot of the space, so git ignores it. bb story materialize and bb story validate read it.

Compose and edit

bb story preflight

Check that the last space sync covers the bloks this plan composes, including preset children and page.

bb story preflight [plan]
FlagWhat it does
--catalog <catalog>A catalog saved with bb catalog --json, such as .backbase/catalog.json, instead of building the project's
--folders <folders>A read-only MCP snapshot of root folders, as { stories: [...] }, to resolve folder translation
--root <root>The project's page blok, page when unset
More

It checks .backbase/plan.json against the last .backbase/space-sync.json for the project’s space: page and every blok the scaffold uses, preset children and required defaults included. A failed component the plan doesn’t use is a note. An absent, failed or skipped one it needs stops compose, and so do missing sync reports and dry runs. A stale preset is reported stale. Pass the same --root as scaffold.

Locale folders. With NEXT_PUBLIC_TRANSLATION_STRATEGY none or field, stories go at the root. With folder, they go in the default locale’s folder (languageMap.defaultLocale): compose saves the root folders, from Storyblok’s MCP, to .backbase/stories/folders.json, each with id, slug, is_folder and parent_id, and passes --folders. Until then the placement is pending, and it exits nonzero.

The result’s placement.parentId goes in the story file as story.parent_id before stamping, and placement.prefix is the full path, such as en/summer-camps. When that folder is missing, compose stops: create it in Storyblok and try again. Compose never creates it, and never falls back to the root.

bb story scaffold

Scaffold the draft story for an approved plan: the planned bloks, leaving Wrap and Create slots for the last stage, as bloks, with a report of what's left to fill.

bb story scaffold [plan]
FlagWhat it does
--sections <sections>The sections.json the plan was made from, if it isn't .backbase/figma/<node-id>/sections.json
--catalog <catalog>A catalog saved with bb catalog --json, such as .backbase/catalog.json, instead of building the project's
--root <root>The story's root blok, the project's page blok. page when unset, as in the boilerplate
--name <name>The story's name. The frame's name when unset
--slug <slug>The story's slug, lowercase words joined by dashes. From its name when unset
--forceScaffold the story again over the files a previous run wrote
More

It builds the approved plan as a story, offline, and writes two files in .backbase/stories/:

  • <slug>.json, { "story": { "name", "slug", "content" } }: the page blok with the Reuse, Compose, Wrap and Create sections in page order, and a backbase-slot where a component is missing. A preset in code starts from its content.
  • <slug>.report.json: what it composed, what it skipped and why (Global and Unsure aren’t composed), the images, every placeholder, the presets, what’s left for a person (todo), and bloks, the Figma layer each blok was built from.

What the plan doesn’t say is filled so the story validates, and the report lists each: copy placeholders that start [Backbase placeholder, empty images with the section’s name as alt, preset links emptied (they point into Backpack’s own space), required links as #backbase-placeholder, and required bloks and options from Backpack’s presets or the first allowed. Then it completes the story as bb story materialize does. Each section’s sb-section gets its name from the Figma section’s name, unique in the story: whats-included, then whats-included-2.

A preset from the space has no content in the catalog, so the report says to apply it in the editor. It never writes over a story it scaffolded before without --force.

bb story fill

Fill a story's copy placeholders, and the empty texts its bloks show, from the frame's copy in sections.json, by node id, order and role, and drop the text-only bloks the frame doesn't show.

bb story fill <file>
FlagWhat it does
--report <report>The scaffold's report, if it isn't the story file's name with .report.json, next to it
--sections <sections>The sections.json to fill from, if it isn't .backbase/figma/<node-id>/sections.json for the plan's frame
--plan <plan>The plan that names the frame, if it isn't .backbase/plan.json
--catalog <catalog>A catalog saved with bb catalog --json, such as .backbase/catalog.json, instead of building the project's
More

It fills a story’s copy placeholders from the frame’s copy, offline, then the empty text its bloks show, such as a button’s label. It pairs each with a text layer by:

  1. the story’s own words: a text that is a layer’s, word for word, is that layer;
  2. node id: a repeated blok, such as a carousel’s slide, takes only its own instance’s layers;
  3. instance order: the n-th card takes the n-th instance’s layers, when that’s the frame’s only run of instances there;
  4. order, between the texts around it;
  5. role: heading, body or label, from the blok and the layer’s text style.

It fills only when exactly one pairing fits, and otherwise lists the candidates. It never invents copy.

  • It drops only what DROPPABLE_BLOKS lists, when the frame doesn’t show it: a tag, an eyebrow before a headline, then a card header the drops left empty. Anything else stays a placeholder, marked the frame has none.
  • It fills an empty field only from SHOWN_TEXT (a button’s label, a link’s text, a tag’s label, a headline’s or text’s content), never a field with words in it or one nobody sees, such as an aria label.
  • A text that looks like a placeholder, such as Heading, is the frame’s own, and is noted.
  • The report’s fill lists filled, fieldsFilled, fieldsLeft, dropped, emptied and left, with the reasons. Running it again changes nothing settled.

bb story materialize

Write a story's content as the Storyblok editor would: every absent field, every design lifted into its stored envelope for its blok's placement, and every colour, size, link and section list in its stored format.

bb story materialize <file>
FlagWhat it does
--catalog <catalog>A catalog saved with bb catalog --json, such as .backbase/catalog.json, instead of building the project's
--datasources <datasources>The space's colour datasources, as bb space datasources writes them, if not .backbase/datasources.json
--offlineUse the cached icon manifest without network access
--sections <sections>Complete only these sections, by the keys bb story diff prints (an sb-section's name, or its _uid when it has none or shares it; :page for the page's own fields), separated by commas, and leave every other as it is. data.outside lists the others completing would change
More

It writes the story as the Storyblok editor would, so Backpack’s wrappers find every field they expect: StickyNavBarSB reads button[0], and the boilerplate’s GalleryImageSB reads blok.design.fields.

  • Absent fields get their birth value: the schema’s default, else an empty value of the field’s type. A value that’s there is never touched.
  • Designs become the lean backpack-breakpoints-v2 envelope, with only the subfields that have a value, and fields always there. The agent’s short dialect, "design": { "gap": { "s": "xl" } }, or one value for every breakpoint, is lifted into it. A subfield the blok’s actual parent doesn’t admit, such as position outside an sb-section, is taken out and reported. An editor’s full envelope is trimmed.
  • Colours are stored as the picker’s selection of a colors or custom-colors entry, with the space’s ids, from .backbase/datasources.json. A hex, a theme token or an unknown name is a color-not-materialized error that names the closest entries.
  • Sizes become { "value", "unit" } (a bare number is vh, as Backpack reads it), and %, vh and vw are clamped at 100. Numbers are clamped to their schema’s bounds. Links become the editor’s link (a placeholder becomes an empty link; another scheme, such as javascript:, is multilink-malformed). Section selectors become the plugin’s selectedSections.
  • An unknown subfield or invalid value (design-unknown-subfield, design-invalid-value) writes nothing: fix it and run it again.

data.report lists every change and why. Running it again writes nothing. Stamp the story after it, never before. --sections <keys> completes only those sections and leaves the rest as they are, to the byte: edit passes it.

Icons. Write a bare kebab name, such as "icon": "fork-knife" (Figma’s ForkKnife). It stores the icon from EF’s icons/bold manifest, fetched once into .backbase/cache/icons-bold.json, which git ignores; --offline forbids the fetch. An unknown name warns (icon-unknown), PascalCase is icon-format, and a field that doesn’t take icons warns (icon-library).

bb story spacing plan

Turn the frame's auto-layout into the spacing values that render it, as an ops envelope for bb story ops apply, with what no field can express.

bb story spacing plan <file>
FlagWhat it does
--plan <plan>The approved plan; uses .backbase/plan.json when present
--report <report>The scaffold's report, which says what the story's body holds; uses <story>.report.json beside the story when present
--sections <sections>sections.json; derived from the plan's frame when unset. Required without a plan
--section <section>Only this exact section name, id, body UID or review key (03-showcase-3-cards); repeat for several sections
--catalog <catalog>A catalog saved with bb catalog --json, such as .backbase/catalog.json, instead of building the project's. Its render table says what each value renders
--out <out>Where the envelope goes; defaults to .backbase/spacing/<story filename>.ops.json
More

It turns the frame’s auto-layout into the spacing values that render it, before anything renders, from the blok map and the render table. It refuses without a render table. It writes nothing to the story: one ops envelope, applied with bb story ops apply.

  • Gaps take the container’s own field (gap, or item_spacing, which pads each row) at the value that renders them, else margins on the children.
  • Padding goes to the blok that pads by default, such as a card body, else the layer’s own blok.
  • Children with no layer, such as icons, and parts of a library instance stand for a frame layer only when they clearly match it.
  • Fill and hug become item_flex, a fixed width an item_basis fraction only within a pixel, and a row frame direction: row.
  • Every px rounds to the nearest token, with its error. A tie goes to the house style’s choice, and is listed in data.ties for a person to rule on. Values land at the frame’s breakpoint (1024px is l), never narrower.
  • data.gaps is what no field can express, and data.skipped what it couldn’t plan, with why. Planning again after the apply changes nothing.

bb story targets

List the bloks bb story ops apply may address: their props, child fields and design options, in pages.

bb story targets <file>
FlagWhat it does
--section <section>Only one section's targets: its name or _uid
--find <find>Only the bloks with this text in a prop, a rich text or a link, case aside
--component <component>Only the bloks of this component
--color <color>Only the bloks painted a colour near this hex (within ΔE00 5), such as #fff5f6
--page <page>The page to show (1 when unset)
--budget <budget>The characters of JSON a page of targets may take (60000 when unset)
--catalog <catalog>A catalog saved with bb catalog --json, such as .backbase/catalog.json, instead of building the project's
More

It lists the offered targets, the only bloks bb story ops apply will touch, each with its uid, component, path, section, and:

  • props: the fields setProp may set, with their values. A rich text lists in lost what mark-up can’t say;
  • children: each bloks field, with the components it takes;
  • design: each design field’s allowed subfields, options and values.

--section, --component, --find <text> and --color <hex> (within ΔE00 5) filter, and combine. It answers in pages of at most --budget characters.

bb story ops apply

Apply an ops envelope to a local story, all or nothing, against the targets offered now, and journal it for undo.

bb story ops apply <file>
FlagWhat it does
--ops <ops>The envelope { "summary", "ops": [...], "warnings": [] }: a file, or - for stdin
--catalog <catalog>A catalog saved with bb catalog --json, such as .backbase/catalog.json, instead of building the project's
--datasources <datasources>The space's colour datasources, as bb space datasources writes them, if not .backbase/datasources.json
More

Ops are how an agent edits a local story: read the targets, write one envelope of typed ops, apply it all or nothing, and journal it so it can be undone. Ops never contact Figma or Storyblok, and change only the local file: the push still needs its plan and your yes.

bb story targets .backbase/stories/package-ils-11.json --section faq
bb story ops apply .backbase/stories/package-ils-11.json --ops ops.json
bb story ops undo .backbase/stories/package-ils-11.json --entry last
bb story validate .backbase/stories/package-ils-11.json --stamp

The envelope

{
  "summary": "Tighten the FAQ and bold the teacher's name",
  "ops": [
    { "op": "updateDesign", "targetUid": "4c9f04cd-…", "changes": { "item_spacing": { "s": "s", "m": null } } }
  ],
  "warnings": []
}

summary is what you say you did; bb measures what changed and journals both. Ops run in order, each with an optional reason. A uid is always an offered target or one minted earlier in the envelope: an agent never writes a _uid.

Op What it does
setProp Sets one of the blok’s props: an option, rich text mark-up, a colour.
updateDesign Sets design subfields at s, m or l, with bb story design set’s checks. null clears one.
insertChild Adds a blok where the field allows it, born whole.
removeNode Removes a blok and everything in it.
reorderChildren Reorders a bloks field: order lists every index once.
moveNode Moves a blok to a field that allows it.
wrapChildren Puts some of a field’s bloks in a new wrapper.
duplicateNode Copies a blok next to itself, with new uids.
unwrapChildren Dissolves a wrapper into its parent.
insertPreset Inserts a preset in code, <component>/<preset name>.

Colours take an entry’s name, "Color/Red/Lightest (100)", or a hex, which becomes the entry with that value, or the nearest within ΔE00 10, as design fit judges. Past that the op is repairable, unless it says "approximate": true. Each approximation is recorded. Without .backbase/datasources.json, the envelope is refused: run bb space datasources.

Rich text takes a small mark-up: a blank line starts a paragraph, a line break is a hard break, **bold**, *italic* or _italic_, and [label](url) to an https: or http: URL, a path, an #anchor or mailto:. A backslash escapes. Mark-up that doesn’t parse is repairable. It has no headings, lists or embedded bloks: a field that holds any refuses mark-up, so send Storyblok’s JSON document instead.

The answer, data.outcome:

  • applied: written, its validation stamp dropped, journalled, with what the diff measured and the new bloks’ mintedUids;
  • unchanged: nothing to do, and not a failure;
  • repairable: nothing written. errors say what to fix, and data.legalTargets lists up to 30 targets to use instead;
  • refused: nothing an envelope could fix.

The skills change a story only through ops, at most four repair rounds an envelope, and never by editing its JSON. Scaffold, fill and materialize are bb’s own generators; a bb test maps every story command to its ops, or the reason it’s exempt.

bb story ops undo

Restore a local story as it was before one ops journal entry, refusing when it changed since unless --force.

bb story ops undo <file>
FlagWhat it does
--entry <entry>The entry to undo: its number in bb story ops log, or last (the default) for the latest not undone yet
--forceRestore it even though the story changed since, losing those changes
--note <note>Why it's undone, kept in the journal
More

It restores the story file as it was before an entry, byte for byte (last by default), drops its stamp, and journals the undo, with undoOf. It refuses (ops-undo-stale) when a later entry or another edit changed the story since: --force restores anyway, and those changes are lost. An undo can’t be undone: apply its ops again.

bb story ops log

List a local story's ops journal: each applied envelope and undo, with what its diff measured.

bb story ops log <file>
FlagWhat it does
--since <since>Show the last N entries (a positive integer)
More

The journal is .backbase/stories/<slug>.ops.jsonl, one line an envelope or undo, with a snapshot of the file before it in .backbase/stories/<slug>.ops/<n>.json. The last 50 entries keep theirs; an older one can’t be undone. A failed write puts everything back.

bb story design get

Show a target blok's design values, breakpoint options and variant fields, and what each value renders as in CSS and px.

bb story design get <file>
FlagWhat it does
--target <target>A section name or _uid, optionally /field.index.field._uid (leading index means content.index)
--catalog <catalog>A catalog saved with bb catalog --json, such as .backbase/catalog.json, instead of building the project's
More

It shows a blok’s design subfields with their values, defaults, types and options at s, m and l, the shapes each accepts, its option fields, and what each spacing value renders: item_spacing: s → item li padding 16px top/bottom (32px between items). [] means the breakpoint isn’t supported. It never contacts Figma or Storyblok.

bb story design get .backbase/stories/package-ils-10.json --target package-hero
bb story design set .backbase/stories/package-ils-10.json --target package-hero gap@s=xl gap@m=l --note "Match the hero spacing"
bb story design revert .backbase/stories/package-ils-10.json --entry last --note "The fit got worse"

Targets start with an exact sb-section.name or any _uid (two matches fail, listing both), then an optional path of fields and indexes: package-hero/content.0.content.1, or package-hero/0.content.1 for short. Percent-encode /, . and % in a name.

bb story design set

Validate targeted design or variant values and apply them as one ops envelope: journal it and drop the validation stamp.

bb story design set <file> <field[@s|m|l]=value...>
FlagWhat it does
--target <target>A section name or _uid, optionally /field.index.field._uid (leading index means content.index)
--catalog <catalog>A catalog saved with bb catalog --json, such as .backbase/catalog.json, instead of building the project's
--datasources <datasources>The space's colour datasources, as bb space datasources writes them, if not .backbase/datasources.json
--note <note>Why this visual iteration changes these values: the envelope's summary
More

An assignment is field[@s|m|l]=value. An exact field name wins, so size=l is the option field and design.size@s=l the design subfield; otherwise design.gap or plain gap. Without a breakpoint it sets every one the field supports; an option field takes none (as=h2). gap@s= clears one breakpoint, gap= all. Object values go in as JSON: 'spacing@s={"pt":"xl","pb":"l"}'.

All assignments are checked together, and one bad one refuses the whole call, changing nothing. Colours and text aren’t set here: use an ops updateDesign or setProp.

A call is one ops envelope and one journal entry, with --note as its summary. It drops the story’s stamp: validate and stamp it again before the next push, even after a revert.

bb story design log

Show the design changes in the ops journal grouped by section; --since N selects the last N entries.

bb story design log <file>
FlagWhat it does
--since <since>Show the last N journal entries (a positive integer)
More

It lists the design values every journal entry changed, sets, envelopes and undos alike, numbered (#3). The design log bb wrote before 0.15, <slug>.changes.jsonl, stays where it was, and bb page compare still reads it.

bb story design revert

Undo one ops journal entry, a design set's included, exactly: ops undo, refusing when the story changed since.

bb story design revert <file>
FlagWhat it does
--entry <entry>The entry to undo: its number in bb story ops log (design set prints it), or last for the latest not undone yet
--catalog <catalog>Accepted as before, and not read: an undo restores the snapshot it took
--note <note>Why this change is reverted, kept in the journal
More

It’s bb story ops undo for one entry, byte for byte, whatever the entry was. It refuses when the story changed since (design-revert-stale: undo the later entries first), or when the entry is an undo, was undone already, or there’s nothing to undo (design-revert). It drops the stamp.

bb story validate

Check a story's content against the story contract (v1) and the project's catalog, before it's written.

bb story validate <file>
FlagWhat it does
--catalog <catalog>A catalog saved with bb catalog --json, such as .backbase/catalog.json, instead of building the project's
--datasources <datasources>The space's colour datasources, as bb space datasources writes them, if not .backbase/datasources.json
--stampOnce the story passes, record its content's hash in .backbase/stories/validated.json, so the plugin's write guard lets exactly this content go to the project's space, from storyblok.config or .backbase/config.json. Not while placeholders are left
--allow-placeholdersStamp the story with placeholders left, once the person has agreed to write it with them
More

It checks a story against the project’s catalog, offline. The file is the content, or a wrapper with it under content or story.content. Each problem has the path of the key to fix, such as content.body[2].content[0].component, a code and a message. Errors fail the story, exit 1; warnings don’t. It also reports the story’s size and its five heaviest sections, which never fail it. It never calls Storyblok: options that live in the space are reported as unchecked.

Code Kind What’s wrong
known-wrapper-crash Error A shape a Backpack wrapper crashes on: an empty sticky nav button.
unknown-component Error A blok the catalog doesn’t have; the message names the closest.
not-root Error The content’s root isn’t a root blok, such as page.
root-only Error A root blok inside another blok.
child-not-allowed Error A blok in a field that doesn’t take it.
min-items Error, or a warning Too few bloks; only a warning when the field is empty and optional.
max-items Error Too many bloks.
missing-required Error A required field left out or empty.
invalid-option Error A value the option field doesn’t offer.
unchecked-option Warning An option from a source the catalog can’t see.
unchecked-children Warning A field that takes bloks by tag or group, so its allowed bloks weren’t checked.
wrong-type Error A value of the wrong shape for its field. Custom fields aren’t checked.
unknown-field Warning, or an error A key the schema doesn’t have; an error when it holds bloks, which would never render.
uid Error A missing, non-string or repeated _uid.
missing-field Error A field the editor always writes is left out.
design-not-materialized Error A design that isn’t the stored envelope.
design-placement Error A design subfield the blok’s placement doesn’t admit.
design-unknown-subfield Error A design subfield the field doesn’t have.
design-invalid-value Error A design value its subfield doesn’t take.
design-dialect Warning Another breakpoints dialect: not checked.
design-unknown-token Warning A colour name not in the picker’s datasource in code.
color-not-materialized Error A colour as a bare string, which renders as nothing.
size-without-unit Error A size as a bare number, which Backpack reads as vh.
size-not-materialized Error A size as a string, such as "600px".
multilink-malformed Error A link that isn’t one as the editor stores it.
sections-not-materialized Error A section selector written as a list.
color-unknown Warning A stored colour the space’s datasource doesn’t have.
number-clamped Warning A number past its bounds; materialize clamps it.
section-unknown Warning A section selector names no section in the story.
house-style Warning A difference from the knowledge pack’s house style; the frame’s value wins.
placeholder Warning A placeholder scaffold left. --stamp won’t stamp while any are left.

bb story materialize clears missing-field, design-not-materialized, design-placement, the size codes, sections-not-materialized, a completable multilink-malformed, and, given the datasources, a named color-not-materialized. The agent fixes the rest: completion never guesses a value. Datasources from another space are refused (datasources-space).

Stamping

--stamp records a story that passes with no errors and no placeholders: the hash of its content in .backbase/stories/validated.json, with the project’s configured space and the story’s parent_id (0 for the root). It takes no --space, so a stamp can’t name another space, and without a configured space it fails. --allow-placeholders stamps with placeholders left, which compose passes only once you’ve agreed. A new stamp drops the stamps for other spaces.

bb story push writes exactly that content to that space, and the write guard lets nothing else through the MCP. Changing the content or the parent folder needs a new stamp; the name and slug don’t.

bb story slug

Give a local story another slug: set story.slug in the file, rename the file after it, and move what bb keeps beside it (its report, push record, ops journal and snapshots, and old design log) to match, so bb story serve, push and the journal find it there.

bb story slug <file> <slug>
More

It drops the story's validation stamp, refuses a slug another local story file has, and never overwrites a file. The one way to change a slug: never edit the story JSON.

One name everywhere

A story file’s name, its story.slug and its slug in Storyblok are always the same: .backbase/stories/<slug>.json holds story.slug: "<slug>", and <slug>.remote.json records the story at that slug. In a folder, the name is the last part of the full slug: en/package-ils is package-ils.json. bb story validate, serve, push, page measure and page compare refuse a story whose names differ (slug-mismatch), with the fix: this command.

It also moves the story’s report, record, ops journal, snapshots and old design log, and drops its stamp: validate, stamp and plan the push again. It refuses a slug another local story has (slug-taken-locally), and never overwrites a file (slug-file-exists). For a story already in Storyblok, the next bb story push moves it there too, shown in the plan as a slug change. Compose and edit run it when their slug is taken, after asking you which.

bb story serve

Serve the local story files as a fake Storyblok Delivery API on 127.0.0.1, each at its story's own slug, so the dev server renders each one fresh on every reload, with an _editable on every blok: nothing is pushed.

bb story serve
FlagWhat it does
--port <port>The port to serve on, 4100 when unset
--stories <stories>The folder of story files, .backbase/stories when unset. Each <slug>.json is served at its story's slug, which is its file's name
--offlineServe the stories that aren't files, such as settings, only from .backbase/cache/serve: never fetch them
--print-envPrint the lines that point the dev server at bb, for .env.local, and don't serve
--write-envWrite those lines to .env.local (never .env), keeping what they replace in a commented block, and don't serve. Only with the person's yes
--restore-envTake bb's lines out of .env.local and put back what they replaced, and don't serve
More

The stories it doesn't have, such as settings, come from the real one once, with the project's public delivery token. It refuses to start when a file's name isn't its story's slug (slug-mismatch) or two files have one slug (serve-conflict): bb story slug fixes both. A story it answers from a file carries story._backbase. --print-env shows the lines that point the dev server at it; --write-env writes them to .env.local, keeping the old values, and --restore-env puts those back.

A draft that was never published is a 404 on localhost, and pushing after every edit is slow. With this running and the dev server pointed at it, the app renders the local story file on every reload, and nothing is pushed.

  • It answers GET /v2/cdn/stories/<full slug> from the story file whose slug it is, read again on every request, in the folder bb story push recorded, or the default locale’s folder for a story with a parent_id. Every blok gets an _editable, so the page has data-blok-uid for measure and compare. A file it can’t read is a 500 that says why, and one line on stderr.
  • Before a story’s first push, nothing checks its slug in the space: a local file can shadow a different story there until bb story push --plan refuses with slug-taken.
  • It refuses to start on slug-mismatch, or two files with one slug (serve-conflict). One that turns up while it runs is answered with a 409, never with Storyblok’s story.
  • Other slugs, such as settings, come once from the real Delivery API, as the draft, with the project’s public token, NEXT_PUBLIC_STORYBLOK_ACCESS_TOKEN, never the Management token. They’re cached in .backbase/cache/serve/; --offline uses only the cache. It never fetches from a URL on this machine.
  • Safety. It listens on 127.0.0.1 only, answers 403 to any other Host header, so a web page can’t reach the drafts, refuses every method but GET and the GraphQL stub’s POST, never writes to Storyblok and logs no requests.
  • A story from a local file carries story._backbase ({ "source": "local", "file", "servedAt" }), and the boilerplate shows its LOCAL DRAFT ribbon, which bb’s captures leave out. GET /_backbase/served?slug=<slug> says what answered a slug last.

--print-env prints the lines that point the dev server at it:

NEXT_PUBLIC_STORYBLOK_DELIVERY_API_URL=http://127.0.0.1:4100/v2
NEXT_PUBLIC_STORYBLOK_GRAPHQL_API_URL=http://127.0.0.1:4100/graphql
NEXT_PUBLIC_CONTENT_VERSION=draft

--write-env writes them to .env.local, keeping the lines they replace; --restore-env puts those back, and removes .env.local only when bb made it. Restart the dev server after either. Both warn when the shell or .env.development.local sets the same names, which Next.js reads first. The compose skill asks before it writes them.

The page What it shows
https://localhost:3000/<slug>/, a story file’s slug The local file as it is now, with the ribbon
Any other slug, such as settings Storyblok’s draft, fetched once and cached
--draft <full slug>, through the preview route, bb’s lines restored Storyblok’s draft, as pushed

bb story pull

Read a story from the project's Storyblok space through the Management API into .backbase/stories/<slug>.json, with a record of what was pulled, for /backbase:edit: never a folder, and never a write to the space.

bb story pull <story>
FlagWhat it does
--forceReplace the local story file and its record even when the file has changes Storyblok doesn't have, or is another story's
More

It reads the story through the Management API, by id or full path, and never writes to the space. It checks what bb doctor --write checks, and refuses a folder (folder), a story the space doesn’t have (story-missing; a path needs its folders, such as en/summer-camps) and a slug that isn’t lowercase words joined by dashes (slug). It writes .backbase/stories/<slug>.json and <slug>.remote.json: the story’s id, slug and full path, updated_at, and a hash of the content and of each section, never a copy of it.

Without --force, it never replaces a local file that may hold work: one changed since the last pull or push (local-edits), one bb has no record of (local-file), or one recorded for another story (story-conflict). /backbase:edit always pulls first.

bb story diff

Compare a story file with the story bb pulled or pushed for it, section by section, through the Management API: what a push would change, and what changed in Storyblok since the pull.

bb story diff <file>
More

Read-only.

A section is a top-level blok in the page’s body, known by its sb-section name, or its _uid when it has none. :page is the page’s own fields and :order the sections’ order. The editor’s own additions to a design, such as its _uid, don’t count as a change; a cleared value does.

  • data.push: your file against Storyblok now, what a push would change.
  • data.remote: Storyblok now against your last pull or push, what someone else changed. changedHere marks a section your file changed too.
  • data.conflicts: the sections in both, what a push would overwrite.

bb story push

Create or update a stamped story as a draft in the project's Storyblok space, through the Management API, never published: --plan says what it would write, --approved <fingerprint> writes exactly that.

bb story push <file>
FlagWhat it does
--planPlan the push without writing: the space, the story, create or update, and a fingerprint the person approves. Saved in .backbase/stories/push-plan.json
--approved <approved>The fingerprint of the plan the person approved, as bb story push --plan printed it. bb plans again and refuses when it has changed since
--story <story>Update this story, by id, instead of creating one: a story the person chose, never a folder. With no record of a pull, nothing guards edits made in Storyblok: bb story pull it first. Without it bb updates only the story it pulled or pushed before
--overwrite-sections <overwrite-sections>Overwrite these sections, which someone changed in Storyblok since the pull, separated by commas: the keys the refused plan and bb story diff print, an sb-section's name, or its _uid when it has none or shares it, :page for the page's own fields, :order for the sections' order. The approval sentence names them
More

It writes a stamped story to the space through the Management API, as a draft, never published (ADR 0028), byte for byte from the file, so no size limit of an MCP call applies. Like Gate 2, it’s a plan you approve by its fingerprint, then the write of exactly that plan.

--plan writes nothing. It checks what bb doctor --write checks, then refuses, exit 1, when:

Code When
not-stamped The content changed since bb story validate --stamp.
stamp-other-space, parent-changed The stamp is for another space, or another parent folder.
publishing-field A field would publish, schedule or release the story.
slug, name No slug to name a record after, or a create without a name.
slug-mismatch The story’s names differ.
parent-not-folder, parent-missing A create’s parent isn’t a folder.
slug-taken Another story has its full path. The error names it and the fix: bb story slug <file> <free-slug>.
folder, story-missing, remote-other-space, story-conflict An update’s story is a folder, isn’t there, is recorded for another space, or isn’t the one --story names.
remote-changed, overwrite-unknown See the push guard.
alt-missing A new image has no alt text.

It creates a story when there’s no record, and updates the one <slug>.remote.json records, or --story <id> names. --story without a record has no push guard: the update replaces the whole content, so /backbase:edit never passes it. An update writes the content only, and the slug after bb story slug. The plan shows where it goes, create or update, whether the story is published (an update leaves the live page as it is until a person publishes), the sections it overwrites, its size, each new image, the fingerprint and the approval sentence, and saves it in .backbase/stories/push-plan.json.

--approved <fingerprint> plans again, and writes only when the fingerprint is the same (plan-changed otherwise), so an edit made in Storyblok after you approved refuses it too. Pass the same --overwrite-sections. It never sends publish, and has no flag for it: --publish is a usage error. A write may take 120 seconds, and a 429 or 5xx is retried four times. A create whose answer was lost is looked up by its path before it’s sent again; an update whose answer was lost is push-unconfirmed: run --plan to see. Then it updates the record.

The push guard

Before an update, --plan reads the story’s updated_at. When it moved since the record, and the push would change a section someone changed there, or undo their reorder, it refuses (remote-changed): Someone changed these sections in Storyblok since your pull. Pull again and make your change on what’s there now, or name each section: --overwrite-sections <keys>, as bb story diff prints them. Each overwrite is named in the plan, the fingerprint and the approval sentence. A save that changed none of your sections only warns.

In Claude Code, the plugin’s Bash hook blocks bb story push without --plan unless its --approved is the fingerprint in push-plan.json, given once, then asks you, naming the story, the space, the sections it overwrites and the approval sentence. It reads the plan in the folder the push runs in: the session’s, or one a plain cd <dir> && at the very start moves into. What it can’t follow for sure falls back to the session’s folder, and approved writes for two folders in one command are blocked. bb space sync and bb assets upload find their plans the same way. The token comes from your shell or the project’s .env, goes only over https to Storyblok’s Management API, and is masked in everything bb prints and writes.

bb assets add

Add an image for one image field of a pulled story, from a local file or a public HTTPS URL, to the story's image manifest for bb assets plan: copied into the project by its hash, nothing uploaded.

bb assets add <source>
FlagWhat it does
--story <story>The story file, .backbase/stories/<slug>.json
--slot <slot>The image field it's for, as <uid>.<field>: a blok bb story targets lists, and a field its schema has as an asset field
--catalog <catalog>A catalog saved with bb catalog --json, such as .backbase/catalog.json, instead of building the project's
--origin <origin>Where the image came from, as the person should read it in the plan, such as "Figma node 12:34". The file's name, or the URL, when unset
--freshStart the story's image manifest over, for a new edit: drop the images added before
More

For /backbase:edit: it adds a new image for one image field, --slot <uid>.<field>, from a local file, a Figma node’s downloaded image or a URL. It checks the slot first, and that the field is an asset field the catalog has, so an image is never uploaded for a field ops would refuse. It sniffs PNG, JPEG, WebP or GIF, never SVG, copies the file into .backbase/edits/<slug>/images/ (ignored), adds it to that edit’s images.json, and uploads nothing.

URLs come only from a public HTTPS host: never HTTP, localhost, or a loopback, private, shared, link-local, unique local, unspecified, multicast or reserved address, an IPv4 one inside IPv6 included. bb resolves the name itself, connects only to an address it checked, follows at most 5 redirects, each checked, and stops reading at 25 MB.

Then the same steps as compose: bb assets plan (into Edits / <story name>, reusing any image the project uploaded before), your yes, bb assets upload, and a setProp with its alt text. The push plan lists the new image.

bb assets plan

Record the resolved project space and plan missing Figma image uploads offline, reusing hashes in the project's asset record or a saved space listing.

bb assets plan <images>
FlagWhat it does
--space-assets <space-assets>A complete MCP snapshot {spaceId, asset_folders: [{id, name, parent_id}], assets: [{id, filename, short_filename, asset_folder_id, ext_id}]}; normalized folder snapshots are also accepted
More

It works offline. It sniffs each file’s format by its content (PNG, JPEG, WebP or GIF; Figma image fills are never SVG), skips any hash .backbase/assets.json already has, and plans the uploads into From Figma / <frame>. The space comes from storyblok.config, then .backbase/config.json, and is recorded for the write guard; stamps for another space block it. A video poster under 5,120 bytes, or transparent or of one colour, is refused as blank.

It saves .backbase/assets-plan.json with a fingerprint that binds the space, the images and their bytes, and an approvalSentence. Compose shows it, with which image goes where and what wasn’t found, in its one approval. A change needs a new plan and a new yes. Commit images.json and .backbase/assets.json; git ignores the downloads, the plans and .backbase/assets-upload.json.

bb assets upload

Upload the approved image plan: sign, transfer, finalize and record each image, skipping recorded hashes.

bb assets upload
FlagWhat it does
--verboseShow finish_upload response shapes without response values or credentials
--approved <approved>The image plan fingerprint approved by the person
More

After your yes, it uploads the plan through the Management API, with doctor’s token and region. The Bash hook blocks a missing or different --approved, also after leading flags such as bb --json assets upload, and asks you before a matching one runs. bb reuses an asset whose filename has the same hash, creates only the folders that are missing, signs each upload, sends it, finalizes it and records it.

It never sends the Management API token to S3, follows no redirects, and never replaces or deletes an asset. When it fails, what’s done stays recorded (data.uploaded): stop before the story, and run it again with the same approval, which skips them. Use data.assets[<imageHash>] as the image’s value, with your alt text.

bb assets record

Record one confirmed upload or reused asset by Figma hash, keeping existing mappings, and return its page envelope.

bb assets record <result>
More

Compose doesn’t need it: bb assets upload records each upload itself. It’s for an asset confirmed some other way.

bb story slots

List every Wrap or Create slot left for the component stage.

bb story slots [story]
More

A Wrap or a Create in the plan becomes a slot: a backbase-slot blok where the missing component goes, including bare section slots. In dev and drafts it’s a dashed box, fan-stack goes here · wrap of FanStack; in published production content it renders nothing.

  • bb doctor --write and bb create install its schema, wrapper, map entry and placements, idempotently.
  • Each slot carries its Figma node, figmaNode in bb story slots --json: its own layer, or its section’s node when the design read has no layer for it.
  • Validation and the push plan list slots, and allow a draft push. Measure, compare and review leave a slot’s geometry out and badge it, until the component is built.

The component stage

/backbase:component, which compose and refine offer before their push, fills the slots:

  1. bb check components --visibility: an existing wrapper is changed only after your yes;
  2. bb component new for each slot, then Chaperone and TypeScript until green, and the catalog again;
  3. bb space plan --components-only, then bb space sync --components-only --approved <fingerprint>, never --ssot. If you approve only part, it stops for a new plan;
  4. each slot replaced through ops, then measured and reviewed;
  5. one draft push, after its own yes.

Each Wrap was approved at Gate 1; each Create needs your yes again. Upstream is offered, and starts only if you ask.

bb story slot

Replace a composed subtree with an undoable Wrap or Create slot, recording the evidence in the plan.

bb story slot <section-or-blok-uid>
FlagWhat it does
--plan <plan>The approved plan; uses .backbase/plan.json when present
--report <report>The scaffold's report, which says what the story's body holds; uses <story>.report.json beside the story when present
--sections <sections>sections.json; derived from the plan's frame when unset. Required without a plan
--section <section>Only this exact section name, id, body UID or review key (03-showcase-3-cards); repeat for several sections
--catalog <catalog>A catalog saved with bb catalog --json, such as .backbase/catalog.json, instead of building the project's
--story <story>The local story file; inferred when exactly one exists
--as <as>wrap or create, as the person decided
--name <name>The planned component's local kebab-case name
--export <export>The Backpack export for a Wrap
--note <note>Why the person chose a component; saved with the measured refine evidence
More
bb story slot <section-or-blok-uid> --story <story.json> --as wrap --name fan-stack --export FanStack --note "Two refine rounds left the same structural height gap"

It replaces a composed section or blok with a slot in one ops envelope, records the Wrap or Create in the plan with the last two measured rounds as what was tried, and bb story ops undo takes both back. You decide every one: refine offers a slot only after two rounds leave a gap spacing can’t explain, and never makes one itself. Review the changed plan at Gate 1.

bb component new

Write a local component the Site Builder way: a Wrap of a Backpack export (--wraps) or a Create from Backpack primitives (--from), with its schema, wrapper, map entry and placements.

bb component new <name>
FlagWhat it does
--wraps <wraps>A Wrap: the Backpack export to wrap, such as FanStack. Backpack's React draws it; the wrapper and schema let Storyblok ask for it
--from <from>A Create: the Backpack primitives it's built from, comma-separated, such as Slider,SegmentedControls,Button
--variant <variant>With --wraps, a prop the wrapper sets, as prop=value: the variant it wraps
--in <in>A parent it's placed in, through backbase.placements.json: repeat it, or comma-separate them. Default: sb-section
--fields <fields>A JSON file of its content fields: Storyblok fields by name, each with an optional "prop" naming the prop it maps to, such as { "items": { "type": "bloks", "component_whitelist": ["sb-image"], "prop": "children" } }. A Wrap without it gets a field for each of the export's props
--dry-runPrint the files it would write, and write nothing
More

A Wrap is a component Backpack’s React has that Storyblok can’t ask for; a Create is new React from Backpack’s primitives. It writes either, in the project only:

bb component new fan-stack --wraps FanStack --in sb-flex-group
bb component new trip-builder --from Slider,SegmentedControls,Button
  • A Wrap writes <Name>SB.tsx, mapping the blok’s fields to the export’s props, and <kebab>.sb.ts. A Create writes <Name>.tsx too.
  • Both get Backpack’s design field, copied, never written by hand, the map entry in component-list.ts, and a place under each --in parent in backbase.placements.json, which one resolver applies when sb-mig syncs. Then it runs components:discover and the project’s Prettier.
  • An optional yes-or-no prop that acts when unset becomes Automatic, On or Off.
  • The wrapper passes its root’s display to getVisibilityClasses as originalDisplayProperty, so Show never turns a grid or flex root into a block.
  • It refuses a name that starts sb- or ends -sb, and one Backpack or the project has already. Nothing goes to Storyblok until Gate 2.

Check

bb check components

Check folder/schema names, map registration with documented exceptions, and that wrappers' visibility keeps their root's display.

bb check components
FlagWhat it does
--namesCheck only folder/schema names
--registrationCheck only registration, honouring documented exceptions
--visibilityCheck only that each wrapper passes getVisibilityClasses its root's display (originalDisplayProperty) when that root isn't a block
More

The Chaperone preset

bb is a dev dependency of the project, pinned to one version, such as "@ef-global/backbase": "0.22.0", and .chaperone.json extends the preset it ships:

{
  "chaperoneVersion": "0.12.0",
  "extends": ["./node_modules/@ef-global/backbase/chaperone/backbase.json"]
}

chaperoneVersion goes in the project’s config: Chaperone rejects it in a preset. It’s a dev dependency, not npx, because Chaperone loads the preset from disk (without it, preset file not found), and so it’s offline, the same in CI, and the same version as the preset. bb doctor --write and bb create set it up: see project-chaperone. Run chaperone check --format ai after changing local components.

A local schema lives in src/components/<PascalCase>/<kebab>.sb.ts, with name: '<kebab>' and a <PascalCase>SB.tsx beside it. The folder’s case must match, on macOS too, and a name never starts with sb-. Every schema needs a map entry or an exception in component-map.exceptions.json, which must exist, and a parent placement.

Rule How Checks
schema-name-matches-folder Native The schema’s file sits in its PascalCase folder, case-exact.
schema-is-registered Native A map entry in component-list.ts, or a documented exception.
wrapper-visibility-keeps-display bb check components --visibility A wrapper whose visibility would turn its grid or flex root into a block.
schema-is-placeable bb check placeable A parent, after resolvers and backbase.placements.json.
flex-child-has-design-field bb check flex-design Local flex children against the installed Backpack’s design options.

The command rules give one result per component, such as schema-is-placeable:fan-stack, so a baseline counts per component. Fix findings rather than baselining them. Without bb installed, each says @ef-global/backbase isn't installed in this project: run npx @ef-global/backbase doctor --write. The preset also rejects rival UI kits, direct Radix imports, raw interactive HTML, Backpack deep imports and signs of copied source, though a rewritten copy gets past, so review still matters. See the preset’s README.

bb check placeable

Check that every local schema has a parent after resolvers and backbase.placements.json.

bb check placeable

bb check flex-design

Check local flex children against the installed Backpack design-field options.

bb check flex-design

bb check pure-tests

Check that every *.pure.ts has a *.pure.test.ts next to it.

bb check pure-tests

bb check commands

Check that bb has one command per file, named after it, each in the registry.

bb check commands
FlagWhat it does
--dir <dir>The commands folder (default: packages/bb/src/commands)

bb check skills

Check that each skill has a SKILL.md with frontmatter and mentions only real bb commands.

bb check skills
FlagWhat it does
--dir <dir>The skills folder (default: plugins/backbase/skills)

Review

bb page measure

Measure each rendered blok against the Figma layer it was built from: exact px deltas for gaps, padding, sizes and alignment.

bb page measure
FlagWhat it does
--url <url>The running app's page URL (http or https); with --draft, the dev server's origin, https://localhost:3000 when unset
--draft <draft>The pushed draft's full slug, such as en/package-ils: opens it through the preview route, with STORYBLOK_PREVIEW_SECRET from the shell, .env.local or .env, masked in all bb shows and writes
--story <story>The saved story JSON, with content.body
--plan <plan>The approved plan; uses .backbase/plan.json when present
--report <report>The scaffold's report, which says what the story's body holds; uses <story>.report.json beside the story when present
--sections <sections>sections.json; derived from the plan's frame when unset. Required without a plan
--section <section>Only this exact section name, id, body UID or review key (03-showcase-3-cards); repeat for several sections
--settle-timeout <settle-timeout>How long, in ms, to wait for the page to finish loading and hold still before failing with page-not-settled (default 20000)
--allow-unsettledCapture a page that didn't settle anyway, with a warning, and mark the report settled: false, so nothing is judged from it. A person's call: the skills never pass it
--catalog <catalog>A catalog saved with bb catalog --json, such as .backbase/catalog.json, instead of building the project's. It says which fields hold copy; without it or Backpack, any text field does
--out <out>Where measure.json and measure.txt go; defaults to .backbase/measure/<story filename>/, and its draft/ with --draft
--iteration <iteration>Add the measure to page compare's iteration, a number or latest, so its report shows the measure table beside the images
--compare <compare>page compare's report directory, for --iteration; defaults to .backbase/compare/<story filename>/, and its draft/ with --draft
--against <against>The kept measure.json to judge this one against, a page compare iteration's: keep, partial or revert for the --target deltas. Needs the catalog's spacing scale
--target <target>With --against: a delta id the try set out to close, <blok uid>/<what> as measure.json has it; repeat for each
--fit-drop <fit-drop>With --against and --iteration: a section's fit falling by more than this is a look at its images, never a revert (default 0.05)
More

Where bb page compare gives a score, this gives exact pixels for every blok: what the frame says against what the page renders.

bb page measure --url https://localhost:3000/package-ils \
  --story .backbase/stories/package-ils.json
  • The right page. It asks bb story serve what answered the page’s slug, and fails unless it was the --story file: served-from-storyblok, served-other-file, not-served or serve-unreachable. With --draft <full slug> it’s the other way round (served-from-local), the page opens through the preview route as compare’s does, and it writes under draft/.
  • The capture. As compare’s, at the frame’s width, once the page settles, with what the page marks data-backbase-ignore hidden. It warns when the frame is exactly a breakpoint wide (768, 1024, 1240 or 1440px): a pixel narrower renders the layout below.
  • Which layer is which blok. The map the scaffold recorded in the story’s report, bloks, else inferred from the copy. A section’s only root blok is the section’s own layer.
  • The deltas, page minus frame, largest first: each gap between children, padding, width and height (labelled hug, fill or fixed), alignment, font size and line height. Under a pixel goes in within. Each section’s total is how far off it is in all.
  • What no number fixes: overflow, a sizing mismatch (frame hug, page fill), and bloks with no layer. Sticky bars are measured where they rest.

measure.txt starts with the section heights, how many are within ±2px of the frame, and how many more within a token step (4px). --iteration writes into compare’s iteration: run bb page compare --iteration auto first.

Judging a try

--against <measure.json> judges this measure against a kept one, and --target <id> names the deltas the try aimed at. For each section:

  • keep: every target moved toward the frame, and nothing else grew by more than a token step;
  • partial: some targets moved, and nothing else grew;
  • revert: none moved, or something else grew (grew).

A token step is the next value on the scale that sets the delta: Backpack’s spacing scale (0, 4, 8, 16, 24, 32, 48, 64, 128 in 4.11.0) for gaps and padding, its type ramp for type, the blok’s sizing field, or 4px, for sizes. A section is done when every delta and its height are within a step, and heightUnexplained when only the height isn’t. A fit that fell by more than --fit-drop sets look: true: look at the images. The fit never changes the decision.

Waiting for the page to settle

Measure, compare and review read a page only once it has settled: the document loaded, the fonts loaded, every image decoded, and the layout and the network still for 500ms. bb turns off smooth scrolling and transitions, makes lazy images eager and scrolls through the page first. Event streams, requests open for 5 seconds or more, and WebSockets don’t count, so hot reload doesn’t hold a page up.

It has 20 seconds, or --settle-timeout. Then it exits 1 with page-not-settled, naming what still moved or loaded, and writes no report. --allow-unsettled captures anyway, marked settled: false, and nothing is judged from it. It’s your call: the skills retry once, then tell you, and never pass it. A page that reloads or stops answering fails either way. See The page didn’t settle.

bb page compare

Compare each rendered story section with its saved Figma shot in an offline report.

bb page compare
FlagWhat it does
--url <url>The running app's page URL (http or https); with --draft, the dev server's origin, https://localhost:3000 when unset
--draft <draft>The pushed draft's full slug, such as en/package-ils: opens it through the preview route, with STORYBLOK_PREVIEW_SECRET from the shell, .env.local or .env, masked in all bb shows and writes
--story <story>The saved story JSON, with content.body
--plan <plan>The approved plan; uses .backbase/plan.json when present
--report <report>The scaffold's report, which says what the story's body holds; uses <story>.report.json beside the story when present
--sections <sections>sections.json; derived from the plan's frame when unset. Required without a plan
--section <section>Only this exact section name, id, body UID or review key (03-showcase-3-cards); repeat for several sections
--settle-timeout <settle-timeout>How long, in ms, to wait for the page to finish loading and hold still before failing with page-not-settled (default 20000)
--allow-unsettledCapture a page that didn't settle anyway, with a warning, and mark the report settled: false, so nothing is judged from it. A person's call: the skills never pass it
--shots <shots>Saved PNG directory; defaults to shots/ beside sections.json
--out <out>Report directory; defaults to .backbase/compare/<story filename>/, and its draft/ with --draft
--iteration <iteration>Save an immutable iteration: a positive integer or auto for the next number; update latest/ and history.json
--selector <selector>CSS selector for order fallback when editable UIDs are absent; defaults to main > *
More

It’s optional: compose offers it, and never runs it on its own.

bb page compare --url https://localhost:3000/package-ils \
  --story .backbase/stories/package-ils.json
  • Local or draft. With bb story serve running, --url shows the local story. --draft <full slug> opens Storyblok’s draft through the boilerplate’s preview route, with STORYBLOK_PREVIEW_SECRET from the shell, .env.local or .env. The secret is masked as *** everywhere bb prints or writes. A missing or too short secret is no-preview-secret; preview-refused, preview-elsewhere and preview-blocked say where the route stopped. Prefer the local route: the preview route may show Backpack’s StatusOverlay, which counts in the score.
  • Which section is which. The scaffold’s report says which sections the story’s body holds; Global and Unsure aren’t on the page. Each body blok is found by data-blok-uid; when none match, by the order of main > *, or --selector, and the report says so. Shots are shots/<node-id>.png, or cropped from frame.png.
  • The capture. One full-page screenshot at the frame’s width, once the page settles, each section cropped from it. Animations run to their end.
  • The report, in .backbase/compare/<story>/: index.html, which opens offline from disk, and compare.json. It exits 0 when at least one section compares: a low score alone never fails.
  • Iterations. --iteration auto writes a new numbered folder, never overwriting one, and latest/ points at the newest capture of every section. Each section gets a verdict against its last scored capture, better, same (within ±0.005) or worse, with the ops journal’s changes since. --section captures only those sections.

The score

fit is 0.4 × pixel similarity + 0.4 × edge agreement (how far edges moved) + 0.2 × height agreement, from 0 to 1, higher is better. Scores are a trend, not approval: fonts, browsers, moving content and white space all move them. Look at the images too.

bb page review

Put each rendered section beside its Figma shot, one image a section, captured by its own element once it has settled in view, worst first.

bb page review
FlagWhat it does
--url <url>The running app's page URL (http or https); with --draft, the dev server's origin, https://localhost:3000 when unset
--draft <draft>The pushed draft's full slug, such as en/package-ils: opens it through the preview route, with STORYBLOK_PREVIEW_SECRET from the shell, .env.local or .env, masked in all bb shows and writes
--story <story>The saved story JSON, with content.body
--plan <plan>The approved plan; uses .backbase/plan.json when present
--report <report>The scaffold's report, which says what the story's body holds; uses <story>.report.json beside the story when present
--sections <sections>sections.json; derived from the plan's frame when unset. Required without a plan
--section <section>Only this exact section name, id, body UID or review key (03-showcase-3-cards); repeat for several sections
--settle-timeout <settle-timeout>How long, in ms, to wait for the page to finish loading and hold still before failing with page-not-settled (default 20000)
--allow-unsettledGo on when the page as a whole didn't settle, with a warning. A section is still captured only once it has settled in view: the review never captures an unsettled section. A person's call: the skills never pass it
--shots <shots>Saved PNG directory; defaults to shots/ beside sections.json
--out <out>The review's folder, where each run is a numbered iteration; defaults to .backbase/review/<story filename>/, and its draft/ with --draft
--measure <measure>The measure.json whose deltas each header counts; defaults to bb page measure's, .backbase/measure/<story filename>/measure.json, when it's of this page
--review-timeout <review-timeout>The whole review's budget, in ms, from opening the browser (default 180000): a section it doesn't get to in time is reported not captured, for another run with --section
--check-notes <check-notes>Check the agent's review.md, opening no page: a ## heading for each section the review folder beside it captured, and each item - [<category>] <what's off, and where> — <outcome>
More

It puts each section of the page beside its Figma shot, one image a section, for the agent to look at what numbers miss: a wrong variant, a colour, a missing gallery. Run it after bb page measure, with the same inputs and the same checks of where the page came from.

  • Each section is found by data-blok-uid, scrolled into view, and captured a viewport at a time once it has settled in view, never in one full-page shot, which can re-lay out a carousel. Fixed and sticky bars outside it are hidden, and sticky ones inside it are pinned where the flow puts them. A failed capture is tried once more.
  • A section that doesn’t settle, has no element, moves while it’s captured or has no Figma shot is not-captured, with its reason; the others are still reviewed. Run it again with --section. The whole review has 3 minutes, or --review-timeout.
  • Each image has the frame on the left, the page on the right, and a header with the height, frame → page, and the measure’s count of deltas. There’s no score. Each run is a numbered folder in .backbase/review/<story>/, worst sections first, slots last.

The notes. For each section, the agent writes what differs in .backbase/review/<slug>/review.md, one line an item: - [<category>] <what's off, and where> — <outcome>. The categories are layout, spacing, typography, colour, imagery, component variant, overflow and content; the outcome open, fixed, design gap or content. --check-notes checks the file, exit 1 with review-notes errors. Compose, refine and edit all review before they push. A visible improvement may keep a change the measure would revert, but only written down, with the reason.

bb page browser install

Install the Chromium bb's page measure and compare use into bb's own cache (about 120 MB); the project and Playwright's shared cache stay untouched.

bb page browser install
More

bb brings its own browser, so measuring a page changes neither your project nor anything else on your machine. It opens pages with the project’s own Playwright when its Chromium is installed, or else its own playwright-core, with Chromium in its own cache, backbase/ms-playwright in your user cache folder. Missing that, it exits 1: browser-missing names the folder and this command, and playwright-missing means bb’s install is incomplete. A Playwright on your PATH never counts.

This installs Chromium’s headless shell only: about 120 MB to download, 280 MB on disk. The skills ask you once first. An explicit PLAYWRIGHT_BROWSERS_PATH wins. bb never installs into Playwright’s shared cache, where an install deletes browsers other projects use, and never adds Playwright to the project.

bb report

Draw the plan, the space, the push and the review as a local HTML page from the run's files; it reads only.

bb report [view]
FlagWhat it does
--openOpen the page in the default browser
--inlinePut the images in the page itself, as data: URIs, so the file stands alone
--slug <slug>The story the review is of; by default the push plan's, or the project's one story
More

It draws the run as one HTML page, beside the agent’s short answer in the chat. It reads only the project’s .backbase/, never writes to Storyblok or Figma, and fetches nothing: no script, and only the page’s own images. It uses the same plain words as the agent (packages/core/src/glossary.pure.ts).

bb report            # every view the run has so far, in .backbase/report.html
bb report plan       # one view, in .backbase/reports/plan.html
bb report all --open # and open it in your default browser
View What it shows
plan (Gate 1) Each section beside its Figma shot, how it gets built and how sure the plan is, then every decision, numbered, with its recommended answer.
space (Gate 2) The components created, updated or left out, the theme’s colours as swatches, the custom colours, the Settings draft and the field plugins.
push Each section new, changed, removed or the same, the images to upload, and where the story goes: always a draft, never published.
review Each section’s review image, its height against the frame’s, and what refine kept or reverted.

It draws every view whose files are there, and says why the rest aren’t. It fails only when none can be drawn (report-missing, report-unreadable), or, for one view, when its files aren’t there. A path that leads out of the project makes its view unreadable. --inline makes the page stand alone, with thumbnails, at most 4 MB of them. The same views are what bb mcp’s widgets show: they’re built in packages/core, from the run’s JSON.

bb mcp

Serve Backbase's MCP tools on stdio until the client goes: show_plan, show_space_plan, show_push_plan, show_review.

bb mcp
FlagWhat it does
--root <root>The folder whose projects the tools read, the current one when unset, empty or an unexpanded ${…}: a project outside it is refused. Never / or the home folder
More

Each reads the run's files as bb report does and shows its view as an MCP Apps widget, with a short card as text for a host without them. They only read: no Storyblok, no Figma, no file written. The plugin's .mcp.json starts it.

The plugin’s .mcp.json starts it as npx -y @ef-global/backbase@<the plugin's version> mcp --root ${CLAUDE_PROJECT_DIR}, so npx needs GitHub Packages in ~/.npmrc, and the first start downloads bb. Each tool is an MCP Apps tool: its _meta.ui.resourceUri links a ui://backbase/<view> page the app draws in a sandboxed frame.

Tool Widget When the skills call it
show_plan ui://backbase/plan Gate 1
show_space_plan ui://backbase/space Gate 2
show_push_plan ui://backbase/push The push plan
show_review ui://backbase/review Review and refine results, and done

Each takes project, the project’s folder (the folder bb mcp runs in when unset), and show_review takes slug, as bb report --slug does. Each returns:

  • Text: the output contract’s short card, the decisions numbered with their recommended answers, then the report page’s path. It’s all an app without MCP Apps shows, the terminal included. Where the app says it draws MCP Apps, the skill’s message leaves out only the line the widget draws, and keeps what you approve: that nothing is written or published yet, what can be lost, closing the Settings story, the decisions, the approval sentence and the report link. The app only says it can draw the widget, not that it did.
  • The view, in the result’s _meta, which never reaches the model: its HTML, with thumbnails inside, at most 2 MB.

A view that isn’t there yet is an error in the contract’s shape: “The plan can’t be shown yet. No .backbase/plan.json yet: Gate 1 writes it.”

Safety.

  • The tools only read (readOnlyHint): no Storyblok, no Figma, no file written. Every secret in the env is masked out of the text and the view.
  • A project outside the root (--root, or the folder it runs in) is refused, links followed, the same way whether or not it exists, so a call can’t probe the disk. It refuses to start with the disk’s root or the home folder as its root. Within a project, bb report’s own rule holds.
  • The widget pages allow no network, in their Content-Security-Policy and _meta.ui.csp, and run one script, allowed by its SHA-256 hash, so a view can’t run one of its own. That script only talks to the app over postMessage: the handshake, the theme, its height, and a Show section N button that sends “show section N” in the chat. There’s no approve button: an approval is still the typed yes, so the write guard and the fingerprints are unchanged.

Upstream

bb component upstream-plan

Plan taking a local Wrap or Create to Backpack: its files in Backpack's layout, its imports, what Backpack expects that it lacks, and the story migration.

bb component upstream-plan <folder>
FlagWhat it does
--backpack <backpack>A fresh clone of ef-global/backpack, in a temp folder: the layout is read from it
--writeWrite the moved files into the clone too (only a clone in the temp folder, never your own checkout)
More

/backbase:upstream takes a component the project built to Backpack in two steps, each with your yes: an issue in ef-global/backpack, then a draft pull request (ADR 0029). This plans it against a fresh clone, writing nothing, and reads Backpack’s layout from the clone itself:

  • Where the files go: a Wrap beside the component it wraps, such as src/components/FanStack/storyblok/FanStackSB.tsx; a Create in a folder of its own.
  • The names get Backpack’s prefix: fan-stack becomes sb-fan-stack, shown as Fan Stack.
  • The imports become Backpack’s own. What can’t, such as the project’s own modules, is listed under unresolved.
  • What’s missing: what to add (presets, tests, a preview image, a story), edit (the story’s parameters.siteBuilder, the package export, and each parent’s whitelist, from backbase.placements.json) and generate (pnpm run components:discover).
  • The rest: the branch, backbase-upstream-<kebab>, its base, the commit message in Backpack’s convention, and the story migration note.

It refuses already-in-backpack, reserved-name, not-a-component, wrapped-unknown, not-backpack and backpack-layout. --write writes into the clone only when it’s a git clone in the temp folder (clone-not-temp), on the plan’s branch (not-on-branch), never over other content (target-exists) or through a symlink out of it.

The two approvals. The agent files the issue only after your yes. It builds the branch, runs Backpack’s lint and tests, commits with its hooks on, and shows you the diff. After a second yes it pushes that branch, and no other, and opens a draft pull request. A yes to the issue never approves the pull request. It never merges, marks it ready, pushes to Backpack’s default or release branches, or runs Backpack’s release. The plugin’s Bash guard blocks merging or marking ready a pull request in ef-global/backpack, through gh, the REST API or GraphQL.

bb request plan

Draft each Gate 1 request as an issue for GCS in ef-global/backpack, or as a comment on an open issue that has it already, for /backbase:upstream to file after a person's yes.

bb request plan [plan]
FlagWhat it does
--issues <issues>The open issues to match, as gh issue list's JSON, in a file or - for stdin: gh issue list -R ef-global/backpack --state open --limit 1000 --json number,title,url,body,state,labels
More

What the design needs and Site Builder doesn’t have goes to GCS, who make Backpack and Site Builder, as an issue in ef-global/backpack, and GCS decides (ADR 0030). This drafts one issue per request from the plan’s drafts. --issues is Backpack’s open issues, as gh issue list -R ef-global/backpack --state open --limit 1000 --json number,title,url,body,state,labels prints them: bb never calls GitHub itself. Each draft is:

  • new: no open issue has it. The body is in .backbase/requests/<key>.issue.md;
  • existing: an open issue has it, by its <!-- backbase-request: <key> --> marker or its title (Padding: 96px, never Padding: 960px). The draft is a comment adding this project’s evidence, so two projects ask once;
  • filed: already recorded.

The approval. The agent shows you every draft, whole, and files only those you say yes to, as an issue or a comment, then records the link. Nothing else in Backpack: no labels created, no assignees, projects or milestones, and no other issue touched. A request you don’t approve stays a draft.

bb request record

Record the link of a request /backbase:upstream filed, in the drafts' file, so no later run files it again.

bb request record <key> <url>
FlagWhat it does
--forceReplace another link the request has recorded already
More

It writes **Filed:** <url> under the request in .backbase/requests/design-gaps.md, so no later run files it again. It refuses a link outside ef-global/backpack (not-a-request-link), an unknown key (request-unknown) and a project with no drafts (requests-missing); a request recorded already is already-recorded, unless --force.

Rules and files

The contracts

Agents write three files, and bb checks each against its JSON Schema and the rules a schema can’t say.

  • sections.json: bb figma sections validate
  • plan.json: bb plan validate
  • a story: bb story validate
More

Agents write these files, and each follows a JSON Schema in the plugin, generated from packages/core. bb checks each against its schema, and against the rules a schema can’t say, such as the order of the sections.

Schema The file Checked by
sections.v1.json .backbase/figma/<node-id>/sections.json, from the frame’s read bb figma sections validate <file>
plan.v1.json .backbase/plan.json, from classifying every section bb plan validate <plan> --sections <file>
story.v1.json A story’s content, such as .backbase/stories/<slug>.json bb story validate <file>

In plan.json, a Wrap is wrap: { export, variant?, reason, backpackRequest? } and a Create create: { name, primitives, schemaNote, backpackRequest }. Both compose their surroundings in structure, with a slot node for the missing component, such as { "slot": { "name": "fan-stack", "kind": "wrap", "export": "FanStack" } }: kebab-case, not sb-, and not a catalog name.

Each Wrap and Create needs tried: [{ component, preset?, why }], real catalog attempts and why each failed. A Wrap may instead have evidence: "library-instance", when the frame has a library instance of the export, and the export has no Storyblok version. Without them it fails, wrap-without-evidence, create-without-evidence or slot-missing; an older plan only warns, bucket-renamed, until it’s classified again. Plans with the old bucket name still load as wrap, with the same warning. You approve every Wrap and Create at Gate 1.

The bucket rules

For each page section, the agent takes the first bucket that fits: Reuse, Compose, Wrap, Create, then Global or Unsure.

  • Design settings come first, and only the catalog decides what exists.
  • Every Wrap and Create needs your yes at Gate 1.
More

For each page section, the agent takes the first of these that fits:

  1. Reuse: one existing blok, with its presets and options. No new code.
  2. Compose: several existing bloks together, each one its parent’s field allows. No new code.
  3. Wrap (was Promote): Backpack’s React has it, Storyblok doesn’t. Its Storyblok version is built locally, and can go to Backpack later.
  4. Create: nothing in Backpack: new React, almost always from Backpack’s primitives, with your yes.
  5. Global: the header or the footer.
  6. Unsure: the agent can’t call it, says why, and asks.

These always hold:

  • Design settings first. A section that differs from a blok only in spacing, colour, visibility or placement is a Reuse or a Compose.
  • Count the main bloks. One main blok, with a section and a heading around it, is a Reuse; two or more different ones are a Compose.
  • Wrap or option. A variant the catalog lists is an option; one only Backpack’s React has is a Wrap.
  • Wrap before Create. When Backpack exports the section’s own component without a wrapper, it’s a Wrap.
  • Global is Global, however the header or footer looks: both live in the site’s settings.
  • Only the catalog decides what exists.
  • Every Create needs your yes. Its name is kebab-case and new, and its primitives are Backpack’s React exports, not *SB wrappers.
  • Build locally, upstream optionally, each step with its own yes. A missing primitive stays a backpackRequest in the plan.
  • Low confidence is said out loud. A call that rests on looks alone is low.

The backbase-classify skill has the full rules and worked examples.

The write guard

The plugin’s hook checks every call to a Storyblok MCP and, in a project, lets through only what compose does.

  • Reads pass. A story write must match its stamp, and assets go only to the project’s space.
  • Publishing and execute_destructive are always blocked.
More

Compose writes the story with bb story push, and reads the space and uploads images through the Storyblok MCP. The plugin’s hook checks every call to a Storyblok MCP server before it runs:

  • Which calls: every tool of a server with storyblok in its name; on any other server, the execute_readonly, execute_mutating and execute_destructive tools, and upload_asset and upload_asset_finish given a space_id. Other tools of a renamed server pass, so keep storyblok in the server’s name.
  • Which project: the nearest folder with .backbase/, up from the agent’s folder or the one Claude Code opened, else one level down. With several projects, only reads pass: run the session from the project’s folder. With none, it isn’t a Backbase context, and every call passes.

In a project, it lets through only what compose does:

  • Reads: search, describe, read-named tools, and execute_readonly of an operation named as a read (get…, list…, retrieve…, fetch…, read…, search…, find…, count…, export…), none for publishing, releases, scheduling or imports. Any other execute_readonly is checked like a write.
  • Writes: execute_mutating of createAssetFolder, createStory, and updateStory of a story compose created through the MCP. The content must match a stamp in validated.json, its parent the stamp’s parent_id, and its space the project’s. Any other write is blocked, such as releaseAll or createStoryScheduling, and so is any tool the guard doesn’t know.
  • Assets: only in the project’s space, with hash-named files and an approved folder. An upload takes only space_id, filename, asset_folder_id, alt, title, copyright, size and ext_id, a finish only space_id and asset_id, and region only when it’s the project’s. Unknown keys, asset updates and deletes are blocked.
  • Always blocked: publishing in any form, execute_destructive, a value given twice with different copies, and any call the guard fails to check.

It reads only those files, has no dependencies and never calls the network. It needs node on your PATH: Claude Code lets a call through when a hook can’t start. Codex and Cursor don’t run it, so there the compose skill makes the same checks itself.

Refining the page

/backbase:refine measures the page against the frame, keeps what gets closer, undoes the rest, and pushes once.

  • It works locally, judged by measured deltas, with the review beside each section.
  • It changes only design values and option fields.
More

/backbase:refine is a measured audit of a story compose stamped. It works locally, and pushes once:

  1. Set up. It checks the story is stamped, asks before bb story serve --write-env, starts bb story serve, and asks you to restart the dev server.
  2. Baseline. bb page compare --iteration auto and bb page measure --iteration latest measure every blok. Then bb page review puts every section beside its Figma shot, and the agent writes down what differs before it chooses any op. It ranks the sections by how far off they are.
  3. The audit. For each section, the agent ranks its deltas and its review items, and picks the values that render the frame’s px (bb story design get, and bb story spacing plan first), as one ops envelope, each op’s reason naming the deltas it targets. Where no value fits, it records the closest available value as a design gap.
  4. Judge it, locally. Compare, then bb page measure --against … --target …: keep, partial (undo, then apply the good part again) or revert (bb story ops undo --entry last). Then bb page review --section … captures the sections it touched, and the agent marks each of their review items fixed, a design gap or content.
  5. Fix-ups. The rest of the budget goes to the sections most off.
  6. Stop. A section stops when it’s within a token step, after two stuck rounds (it offers a slot, and you decide), after 3 tries in a row that move nothing, or after 5 tries; 15 in all. Every section that’s off gets one try before any gets a second. --tries, --budget and --fit-drop change these.
  7. The report. A last compare, measure, and review of every section: each section’s review items and how each ended, its px off before and after, the envelopes kept and undone, and what’s left, each with a reason: a design gap, an approximation (the nearest value), a field that doesn’t reach it, structure or copy.
  8. Push once, after your yes. Then it stops the server, restores .env.local, and measures the pushed draft with --draft.

Refine changes only design values and option fields, never content or structure (that’s /backbase:edit), never the file by hand, and never a gap Gate 1’s design fit found. Refine skips slots, and offers one only after two stuck rounds. When someone changes the story in Storyblok meanwhile, the push plan refuses, and refine stops rather than overwrite their sections. pnpm eval runs a scripted dry run of it.