Writing a recipe package
A recipe package is an npm package whose package.json describes its recipes under a chernabog key. @chernabog/recipes is one. A company’s package works exactly the same way: publish it to any registry, public or private, and every team installs it in their .chernabog/ and copies or imports its recipes.
The format
Section titled “The format”acme-chernabog-recipes/├── package.json├── tsconfig.json├── flows/│ └── deploy.ts├── prompts/│ └── deploy.md└── stages/ └── smoke-test.ts{ "name": "@acme/chernabog-recipes", "version": "2.1.0", "type": "module", "files": ["flows", "prompts", "stages"], "peerDependencies": { "@chernabog/sdk": "^0.8.0" }, "devDependencies": { "@chernabog/sdk": "0.8.0", "@types/bun": "^1.3.0", "effect": "4.0.5", "typescript": "^5.9.3" }, "chernabog": { "recipes": { "deploy": { "description": "Deploys a pull request once it merges", "flow": "flows/deploy.ts", "prompts": ["prompts/deploy.md"], "stages": ["stages/smoke-test.ts"], "docs": "https://wiki.acme.example/chernabog/deploy" } } }}| Key in a recipe | What it is |
|---|---|
description |
One line, for the list a person chooses from |
flow |
The recipe’s flow, under flows/. Its default export is a defineFlow(...), as a repo’s own flow is |
prompts |
The prompts it renders, under prompts/ |
stages |
The steps it runs with step(...), under stages/ |
docs |
Where it’s documented; recipes list shows it |
The files keep the layout of a repo’s .chernabog/. A copy lands at the same path, and an imported flow finds its prompts and steps in the package, after any of the repo’s own. A recipe is named after its key, so this one is @acme/chernabog-recipes/deploy.
A flow imports @chernabog/sdk and effect like any flow. At runtime it runs on the SDK the repo installed, and chernabog substitutes its own copies of @chernabog/core and effect, whatever is installed. Any other package a recipe imports goes in its dependencies.
The SDK range
Section titled “The SDK range”The @chernabog/sdk peer dependency says which SDK versions the package’s flows are written against. It’s the contract between your package and the repos that install it:
chernabog init,recipes addandrecipes updatepick the newest version of your package whose range includes the repo’s SDK pin. A repo on an older SDK keeps getting the last version that supported it.- A repo that imports from your package is refused before any run starts, as
incompatibleand naming your package, if your range doesn’t include the SDK the repo has installed.
Which binaries run your flows is the SDK’s business, not yours: the SDK declares the @chernabog/core it needs, and the binary provides core. A package that imports @chernabog/core directly can declare a core range too, and the binary checks it the same way.
Widen the range when a new SDK runs your flows unchanged. Move it, in a release of its own, when a recipe needs a newer SDK.
Step names are public API
Section titled “Step names are public API”A run in flight resumes by the names of the steps it finished: claim, local-gate-1, deploy-2. A new version of your package reaches every repo that imports from it at once, including runs that are waiting on a review or on CI. Renaming a step, or changing the id a step is given, makes those runs do that step again when they resume.
Renaming a step is a breaking change. Release it as a new major version, or, before 1.0, a new minor. Adding a step or changing what one does is not.
Type-checking
Section titled “Type-checking”Install @chernabog/sdk, effect and @types/bun as dev dependencies, at the versions you write against, and check the flows with tsc --noEmit. packages/recipes/tsconfig.json is a tsconfig you can copy:
{ "compilerOptions": { "target": "esnext", "module": "preserve", "moduleResolution": "bundler", "allowImportingTsExtensions": true, "noEmit": true, "strict": true, "skipLibCheck": true, "types": ["bun"] }, "include": ["flows/**/*.ts", "stages/**/*.ts"]}Rehearsing
Section titled “Rehearsing”chernabog rehearse runs a flow end to end on a seeded repo, with stand-ins where the agent would be. To rehearse your package before you publish it, enroll a scratch repo that imports the recipe, and pass your package’s checkout with --package:
# the scratch repo's .chernabog/config.yamlflows: deploy: from: "@acme/chernabog-recipes/deploy"chernabog rehearse deploy --repo scratch --scenario .chernabog/rehearsals/deploy.yaml \ --package ~/code/acme-chernabog-recipesThe rehearsal loads the flow, its prompts and its steps from your checkout, in place of any version installed in the scratch repo, and still checks your SDK range. Your edits take effect without a publish.
Publishing
Section titled “Publishing”Publish it like any npm package: npm publish, to npm or to your company’s registry. Teams install it in .chernabog/ (bun add -d @acme/chernabog-recipes), with the registry and its auth in .chernabog/.npmrc. chernabog recipes add --import moves it to dependencies for a flow they import.
Use semver as the step-name rule above says. A patch or a minor that keeps every step name is safe to take in every repo at once, and a team that wants to hold back pins the version in its lockfile.