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:newand/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.localwith 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.
--jsonprints 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
| Flag | What it does |
|---|---|
--json | Print one JSON envelope instead of text |
-h, --help | Print help |
-v, --version | Print 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| Flag | What it does |
|---|---|
--fix | Apply the fixes bb can make itself, then list the manual steps left |
--write | Add 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, orproject-storyblok-tokenwhen the token is in the project’s.envproject-backpack,project-storyblok-tokenandproject-env-ignored. A shell token that overrides a different one in.envonly warns, and doesn’t block:space-accesschecks the token sb-mig will usespace-idandspace-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-mcpis 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-tokenpasses withCovered 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, thenspaceIdinstoryblok.config,space.idin.backbase/config.json,STORYBLOK_SPACE_IDin 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.configasbb catalogdoes. Like sb-mig, it loads.envfirst without overriding your shell. - It asks the Management API sb-mig uses:
storyblokApiUrlinstoryblok.config, thenNEXT_PUBLIC_STORYBLOK_MANAGEMENT_API_URL, then the EU API.space-accessgives Storyblok 10 seconds to answer. - The token goes only over https, and only to Storyblok’s Management API: EU
https://mapi.storyblok.com/v1, UShttps://api-us.storyblok.com/v1, CAhttps://api-ca.storyblok.com/v1, APhttps://api-ap.storyblok.com/v1, CNhttps://app.storyblokchina.cn/v1(andlocalhostor127.0.0.1, for tests). Any other host, or plain http, failsspace-accessand 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>| Flag | What 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 versionbb help
Print help, for bb or for one command.
bb help [command...]bb droid
Roll bb's droid across the terminal.
bb droidPlan
bb catalog
List this project's Backpack and local components, placements, presets and problems.
bb catalog| Flag | What it does |
|---|---|
--component <component> | Show one component by its schema name |
--knowledge | Compile 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-stale | With --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:
$ooption sets,$csets of bloks,$ddesign subfields,$psubfields 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. --sectionsadds a colour map: each semantic variable the frame uses, and thecolorsentry 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 acolorsentry 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 markedCURATED. 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 aposition’s numbers are at each breakpoint (in 4.11.0, lines 0–6 atsandm, 0–16 froml, full bleed[0,6]and[0,16]), and which field moves a flex box’s children which way (insb-flex-group,alignis the vertical one).- It’s scoped to what the root blok holds,
pageunless--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
--catalogrefuses 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. --outwrites through a temporary file, so nothing reads half a catalog.--if-stalerebuilds only when the fingerprint differs, and says why indata.rebuiltanddata.why.- The skills rebuild both before they use them, and tell you in one line.
bb doctor’scatalogcheck 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| Flag | What 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 |
--measure | Measure 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>| Flag | What 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>| Flag | What 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>| Flag | What 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-fit | Store 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 isxxxl, 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>| Flag | What it does |
|---|---|
--full | Show each section's whole structure, not just 2 levels below its root |
--questions | List each section's questions under the table |
--approximations | List 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| Flag | What 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-only | Plan 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:
- checks what
bb doctor --writechecks, and stops when the project isn’t ready; - checks the project’s sb-mig is
6.5.0-beta.25or later (the boilerplate pins one that is); - runs
sb-mig sync datasources --all --dry-run --json, thensb-mig sync components --all --presets --dry-run --json, with nothing on stdin and the checked space asSTORYBLOK_SPACE_ID; - with
--sections, plans the theme; - saves
.backbase/space-plan.jsonand 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 exactbb space synccommand.
- Destructive changes can lose content:
field-removed,likely-rename,type-changedandwhitelist-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.
--excludedrops a component and its destructive changes, and lists it inplans.excludedwith your reason. The fingerprint covers the names, not the reasons. An unknown name isexclude-unknown.- Field plugins. Each plugin a component’s top-level custom fields use is
available,missingorunverified. It’smissingonly 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 inmissingFieldPlugins, with the way round: install it, or--excludethe component. --components-onlyplans 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:
setwhen the token has nouservalue,changewhen it has another,resetwhen the default is already the frame’s colour.- The value is
var(--color-<primitive>)for an exactcolorsmatch, else the hex, such as#E0D7F9. It never snaps to the nearest. - Each colour of the frame’s own is offered as a
custom-colorsentry, such asPackage (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
uservalue it replaces is lost from the space, so it’s named in theapprovalSentence, listed with the destructive changes and named in the hook’s ask..backbase/space-sync.jsonrecords eachpreviousvalue. - For each Settings story (
settings, or<locale>/settings), the plan shows each token whoseselectedvalue 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 syncrefuse.
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| Flag | What it does |
|---|---|
--components-only | Plan 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-destructive | Allow 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-failedfor each component sb-mig couldn’t write, with--excludeas the way round (a missing field plugin’s 422 names the plugin, andfieldPluginFailureslists it);component-not-syncedwhen sb-mig’s report lacks a component it was given;sync-failedfor 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 istheme-write-failed, indata.theme. - Settings is saved with only its
themefield changed, as a draft, never published.space-sync.jsonrecordspreviousandpreviousModel, 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 datasourcesMore
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]| Flag | What 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]| Flag | What 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 |
--force | Scaffold 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 abackbase-slotwhere 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), andbloks, 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>| Flag | What 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:
- the story’s own words: a text that is a layer’s, word for word, is that layer;
- node id: a repeated blok, such as a carousel’s slide, takes only its own instance’s layers;
- instance order: the n-th card takes the n-th instance’s layers, when that’s the frame’s only run of instances there;
- order, between the texts around it;
- 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_BLOKSlists, 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, markedthe 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
filllistsfilled,fieldsFilled,fieldsLeft,dropped,emptiedandleft, 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>| Flag | What 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 |
--offline | Use 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-v2envelope, with only the subfields that have a value, andfieldsalways 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 aspositionoutside ansb-section, is taken out and reported. An editor’s full envelope is trimmed. - Colours are stored as the picker’s selection of a
colorsorcustom-colorsentry, with the space’s ids, from.backbase/datasources.json. A hex, a theme token or an unknown name is acolor-not-materializederror that names the closest entries. - Sizes become
{ "value", "unit" }(a bare number is vh, as Backpack reads it), and%,vhandvware 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 asjavascript:, ismultilink-malformed). Section selectors become the plugin’sselectedSections. - 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>| Flag | What 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, oritem_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 anitem_basisfraction only within a pixel, and a row framedirection: 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.tiesfor a person to rule on. Values land at the frame’s breakpoint (1024px isl), never narrower. data.gapsis what no field can express, anddata.skippedwhat 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>| Flag | What 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 fieldssetPropmay set, with their values. A rich text lists inlostwhat 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>| Flag | What 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 --stampThe 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.errorssay what to fix, anddata.legalTargetslists 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>| Flag | What 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 |
--force | Restore 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>| Flag | What 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>| Flag | What 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...>| Flag | What 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>| Flag | What 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>| Flag | What 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>| Flag | What 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 |
--stamp | Once 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-placeholders | Stamp 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| Flag | What 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 |
--offline | Serve the stories that aren't files, such as settings, only from .backbase/cache/serve: never fetch them |
--print-env | Print the lines that point the dev server at bb, for .env.local, and don't serve |
--write-env | Write 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-env | Take 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 folderbb story pushrecorded, or the default locale’s folder for a story with aparent_id. Every blok gets an_editable, so the page hasdata-blok-uidfor 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 --planrefuses withslug-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/;--offlineuses only the cache. It never fetches from a URL on this machine. - Safety. It listens on
127.0.0.1only, answers 403 to any otherHostheader, so a web page can’t reach the drafts, refuses every method butGETand the GraphQL stub’sPOST, 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 itsLOCAL DRAFTribbon, 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>| Flag | What it does |
|---|---|
--force | Replace 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.changedHeremarks 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>| Flag | What it does |
|---|---|
--plan | Plan 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>| Flag | What 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 |
--fresh | Start 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>| Flag | What 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| Flag | What it does |
|---|---|
--verbose | Show 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 --writeandbb createinstall its schema, wrapper, map entry and placements, idempotently.- Each slot carries its Figma node,
figmaNodeinbb 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:
bb check components --visibility: an existing wrapper is changed only after your yes;bb component newfor each slot, then Chaperone and TypeScript until green, and the catalog again;bb space plan --components-only, thenbb space sync --components-only --approved <fingerprint>, never--ssot. If you approve only part, it stops for a new plan;- each slot replaced through ops, then measured and reviewed;
- 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>| Flag | What 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>| Flag | What 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-run | Print 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>.tsxtoo. - Both get Backpack’s design field, copied, never written by hand, the map entry in
component-list.ts, and a place under each--inparent inbackbase.placements.json, which one resolver applies when sb-mig syncs. Then it runscomponents:discoverand 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
getVisibilityClassesasoriginalDisplayProperty, 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| Flag | What it does |
|---|---|
--names | Check only folder/schema names |
--registration | Check only registration, honouring documented exceptions |
--visibility | Check 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 placeablebb check flex-design
Check local flex children against the installed Backpack design-field options.
bb check flex-designbb check pure-tests
Check that every *.pure.ts has a *.pure.test.ts next to it.
bb check pure-testsbb check commands
Check that bb has one command per file, named after it, each in the registry.
bb check commands| Flag | What 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| Flag | What 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| Flag | What 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-unsettled | Capture 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 servewhat answered the page’s slug, and fails unless it was the--storyfile:served-from-storyblok,served-other-file,not-servedorserve-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 underdraft/. - The capture. As compare’s, at the frame’s width, once the page settles, with what the page marks
data-backbase-ignorehidden. 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’stotalis 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| Flag | What 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-unsettled | Capture 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 serverunning,--urlshows the local story.--draft <full slug>opens Storyblok’s draft through the boilerplate’s preview route, withSTORYBLOK_PREVIEW_SECRETfrom the shell,.env.localor.env. The secret is masked as***everywhere bb prints or writes. A missing or too short secret isno-preview-secret;preview-refused,preview-elsewhereandpreview-blockedsay where the route stopped. Prefer the local route: the preview route may show Backpack’sStatusOverlay, 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 ofmain > *, or--selector, and the report says so. Shots areshots/<node-id>.png, or cropped fromframe.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, andcompare.json. It exits 0 when at least one section compares: a low score alone never fails. - Iterations.
--iteration autowrites a new numbered folder, never overwriting one, andlatest/points at the newest capture of every section. Each section gets a verdict against its last scored capture,better,same(within ±0.005) orworse, with the ops journal’s changes since.--sectioncaptures 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| Flag | What 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-unsettled | Go 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 itsreason; 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 installMore
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]| Flag | What it does |
|---|---|
--open | Open the page in the default browser |
--inline | Put 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| Flag | What 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
projectoutside 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 overpostMessage: 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>| Flag | What it does |
|---|---|
--backpack <backpack> | A fresh clone of ef-global/backpack, in a temp folder: the layout is read from it |
--write | Write 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-stackbecomessb-fan-stack, shown asFan 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, frombackbase.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]| Flag | What 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, neverPadding: 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>| Flag | What it does |
|---|---|
--force | Replace 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 validateplan.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:
- Reuse: one existing blok, with its presets and options. No new code.
- Compose: several existing bloks together, each one its parent’s field allows. No new code.
- Wrap (was Promote): Backpack’s React has it, Storyblok doesn’t. Its Storyblok version is built locally, and can go to Backpack later.
- Create: nothing in Backpack: new React, almost always from Backpack’s primitives, with your yes.
- Global: the header or the footer.
- 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
*SBwrappers. - Build locally, upstream optionally, each step with its own yes. A missing primitive stays a
backpackRequestin 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_destructiveare 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
storyblokin its name; on any other server, theexecute_readonly,execute_mutatingandexecute_destructivetools, andupload_assetandupload_asset_finishgiven aspace_id. Other tools of a renamed server pass, so keepstoryblokin 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, andexecute_readonlyof an operation named as a read (get…,list…,retrieve…,fetch…,read…,search…,find…,count…,export…), none for publishing, releases, scheduling or imports. Any otherexecute_readonlyis checked like a write. - Writes:
execute_mutatingofcreateAssetFolder,createStory, andupdateStoryof a story compose created through the MCP. The content must match a stamp invalidated.json, its parent the stamp’sparent_id, and its space the project’s. Any other write is blocked, such asreleaseAllorcreateStoryScheduling, 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,sizeandext_id, a finish onlyspace_idandasset_id, andregiononly 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:
- Set up. It checks the story is stamped, asks before
bb story serve --write-env, startsbb story serve, and asks you to restart the dev server. - Baseline.
bb page compare --iteration autoandbb page measure --iteration latestmeasure every blok. Thenbb page reviewputs 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. - 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, andbb story spacing planfirst), as one ops envelope, each op’sreasonnaming the deltas it targets. Where no value fits, it records the closest available value as a design gap. - 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). Thenbb page review --section …captures the sections it touched, and the agent marks each of their review items fixed, a design gap or content. - Fix-ups. The rest of the budget goes to the sections most off.
- 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,--budgetand--fit-dropchange these. - 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.
- 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.