Your first schematic
Every step below is runnable exactly as written. You’ll initialise a workspace, scaffold a schematic, declare its typed inputs, write the factory, and execute it — twice, to see idempotency at work. Install the CLI and Bun first if you haven’t.
-
Initialise a workspace (inside any project — new or existing):
Terminal window builder initThis creates
project-builder.json(the workspace config), aschematics/folder, and an AI skill under.claude/skills/pbuilder/— a command reference your coding agent picks up automatically, no wiring needed. It also installs@pbuilder/sdkas a dev dependency, creating apackage.jsonfirst if the folder doesn’t have one. -
Scaffold a schematic:
Terminal window builder new schematic helloYou get a registered, ready-to-edit package:
schematics/hello/├── schema.json # typed inputs — the contract with the user├── schema.generated.ts # generated from schema.json — never edit└── factory.ts # your authoring logicproject-builder.jsonnow lists it undercollections.default.hello. -
Declare the inputs — replace the generated
schema.json’s contents with the following; every property needs alabel(codegen fails without it):schematics/hello/schema.json {"properties": {"name": { "type": "string", "label": "Service name", "required": true }}}Property types are
string,number,boolean, andenum(which requires a non-emptychoicesarray);defaultanddescriptionare optional. -
Regenerate the input types:
Terminal window bunx pbuilder-codegen schematics/helloThis rewrites
schema.generated.tswith anInputtype derived from your schema — your factory is typed against the schema, never against a hand-written shape. -
Write the factory:
schematics/hello/factory.ts import { create, find, replaceContent } from "@pbuilder/sdk/commons";import type { Input } from "./schema.generated.ts";export default async (input: Input) => {// create a new filecreate(`src/services/${input.name}.ts`, {template: `export const serviceName = "${input.name}";`,options: {},});// content-aware edit: read the tree, then create or updateconst existing = await find("services.txt").read();if (existing === undefined) {create("services.txt", { template: input.name, options: {} });} else {replaceContent("services.txt", `${existing}\n${input.name}`);}};The engine invokes the module’s default export — that’s the factory. This one builds its output strings in TypeScript; the template language is the declarative alternative.
-
Execute it —
default:hellois<collection>:<schematic>as registered inproject-builder.json; inputs are passed as CLI flags:Terminal window builder execute default:hello --name=payments~ services.txt~ src/services/payments.ts✓ done — 2 modified(
~marks a path the run wrote.)
Run it again
Section titled “Run it again”Now run it again with a different input:
builder execute default:hello --name=ordersservices.txt grows by one line instead of being clobbered — that’s the read-back loop
from step 5 doing its job. Re-running with the same name, on the other hand, rejects
with path-collision: create is fail-closed on an existing path (pass force: true to
overwrite deliberately), and since a failed run writes nothing, your tree is left exactly
as it was.
Make mutations idempotent. Factories re-run against already-generated projects — check whether your marker, import, or entry is already present before inserting it. The factory above shows the read-back mechanics; a production factory would also skip the append when the name is already listed.
If your editor flags the .ts-extension import or bun:test, that’s just missing
tsconfig settings — see Testing schematics; Bun itself runs
fine without them.
Two rules that save debugging time
Section titled “Two rules that save debugging time”- Always pass
options: {}tocreate, even when the template has no tokens. The type requires it — and in untyped call sites (plain JS,any) omitting it putsundefinedin the directive batch, rejecting the write as unrepresentable. find().read()is a trichotomy:undefinedmeans the file is absent,""means it exists and is empty. Branch on=== undefined— neverif (!content), which conflates the two.
Next steps
Section titled “Next steps”- Test it without the CLI or a running engine — Testing schematics.
- Mutation verbs — everything a factory can do to the tree.
- Templates — the declarative template language inside
create(). - Scaffolding — mirror a whole folder of templates instead of
writing one
create()per file. - Dry-run — preview a factory’s planned changes before anything commits.
- Error handling — the structured
AuthoringErrorcontract of a rejected run.