Your first plugin
In this tutorial you create the plugin bb plugin new generates, install it into your bb, use it from three places, and change it while bb reloads it. The scaffold is a working todo list with one store and three ways in: a page in the sidebar, the bb hello command, and a skill that tells the agent to use that command. Every part of it maps onto a part of How a plugin works, which this tutorial assumes you have read.
You need a running bb 0.43 and npm. Keep the UI zone map open alongside if you want to see where each surface sits in the window.
1. Create the plugin
Section titled “1. Create the plugin”$ bb plugin new helloCreated bb-plugin-hello/ (bb-plugin-hello).Installed dependencies (npm install).Next steps: cd bb-plugin-hello bb plugin install .The command installs the dependencies itself. If that step fails, the output lists npm install --include=dev among the next steps; run it before step 5.
The command takes no flags. The package is named bb-plugin-hello, and bb derives the plugin id hello from that name by dropping the bb-plugin- prefix. The id appears in the page’s URL, in the storage path and as the CLI command name.
2. Find the entries in the scaffold
Section titled “2. Find the entries in the scaffold”The scaffold holds four files that make up the plugin and three directories of UI code you own.
package.jsonis the manifest: the npm identity plus thebbblock, which namesserver.tsandapp.tsxas the two entries.server.tsis the server entry: the todo store, four RPC methods and thebb hellocommand.app.tsxis the app entry: the “Example todos” page in the sidebar.skills/example-todos/SKILL.mdtells the agent whichbb hellocommands exist and when to use them.components/ui/,hooks/andlib/hold vendored shadcn components and helpers, which are yours to change.
Every generated file, the manifest fields and the dependency rule are in Package anatomy.
3. Read the server entry
Section titled “3. Read the server entry”Open server.ts. Stripped to its structure, it does this:
// server.ts, abridgedexport const rpcContract = defineRpcContract({ todos_list: { input: z.null(), output: z.object({ todos: z.array(todoSchema) }) }, todos_add: { input: z.object({ title: z.string().trim().min(1).max(200) }), output: todoSchema }, // todos_set_done, todos_remove});
export default async function plugin(bb: BbPluginApi) { const writeTodos = async (todos: Todo[]) => { await bb.storage.kv.set("todos", todos); bb.realtime.publish("todos-changed", { count: todos.length }); };
bb.rpc.register(rpcContract, { todos_list: …, todos_add: …, /* … */ }); bb.cli.register({ name: "hello", commands: [/* list, add, done, undo, remove */], async run(argv) { … } }); bb.onDispose(() => { bb.log.info("disposed"); });}The default export is the factory from How a plugin works. It registers four RPC methods and one CLI command, and both call the same helpers over bb.storage.kv. Every write ends with bb.realtime.publish, which tells every open page that the list changed. The contract is exported so that app.tsx can import its type.
4. Read the app entry
Section titled “4. Read the app entry”Open app.tsx. The page asks the server for the list over RPC and asks again whenever the signal arrives:
// app.tsx, abridgedfunction useTodos() { const rpc = useRpc<typeof rpcContract>(); // typed by the server's contract // rpc.call("todos_list") on mount, and again on every "todos-changed" signal useRealtime("todos-changed", refetch); …}
function TodosPage() { const { rpc, todos, refetch } = useTodos(); …}
export default definePluginApp((app) => { app.slots.navPanel({ id: "example-todos", title: "Example todos", icon: "ListTodo", path: "example-todos", component: TodosPage, // route /plugins/hello/example-todos });});app.slots.navPanel adds a row to bb’s sidebar and gives the plugin a full page. The file imports only the type of rpcContract, so no server code reaches the browser bundle. React and the SDK are not bundled either: bb supplies them at runtime, which is why app.tsx works only inside bb.
5. Install it and use it from three places
Section titled “5. Install it and use it from three places”cd bb-plugin-hellobb plugin install . # a path install: bb loads server.ts directly and builds only app.tsxbb plugin dev # leave running: rebuilds and reloads on every savebb plugin install warns that plugins run with full trust and asks for confirmation; answer y. Without a terminal to ask in, it refuses unless you pass --yes. After the install, bb plugin list shows hello@0.1.0 running.
Now use the plugin the three ways the scaffold wires up:
- In bb, open Example todos in the sidebar and add a todo.
- In a terminal, run
bb hello add "Ship it". The page shows the new todo without a reload, because the command’s write publishedtodos-changed. - In a thread, ask the agent to add a todo. The skill tells it to run
bb hello add, so the change takes the same path as your terminal command.
app.tsx · navPanel “Example todos”
/plugins/hello/example-todos
rpc.call("todos_add", …)
useRealtime("todos-changed", refetch)
1↓ rpc.call(…) → POST /api/v1/plugins/hello/rpc/<method>
POST /api/v1/plugins/hello/rpc/<method>
input validated by the schema
{ ok:true, result } | { ok:false, error }
↓ server.ts — jiti, same process
server.ts — jiti, same process
bb.settings.define({ showDone })
bb.rpc.register(rpcContract, …)
bb.cli.register({ name: "hello" })
bb.realtime.publish("todos-changed")
bb.onDispose(() => …)
↓ read and write → bb.storage.kv → plugin_kv in bb.db
4↓ publish to every client → WebSocket /ws — { type:"plugin-signal", pluginId:"hello", channel:"todos-changed", payload } (signal back)
bb hello add "Ship it"
run() executes on the server, not in the CLI
2↓ CLI → server → POST /api/v1/plugins/hello/cli
POST /api/v1/plugins/hello/cli
{ argv, cwd?, threadId? }
↓ server.ts — jiti, same process
the agent in the thread
skills/example-todos/SKILL.md is injected into the thread
3↓ the same command → the same bb hello command
the same bb hello command
the agent never reaches the store directly
↓ server.ts — jiti, same process
bb.storage.kv → plugin_kv in bb.db
key "todos", JSON ≤ 256 KB
beside it: <dataDir>/plugins/hello/
WebSocket /ws — { type:"plugin-signal", pluginId:"hello", channel:"todos-changed", payload }
ephemeral: nothing is stored and nothing is replayed; the client does the filtering, so this is not a confidentiality boundary
↓ every page refetches → app.tsx · navPanel “Example todos” (signal back)
All three paths end in the same server.ts, and none of them touches the store directly. bb plugin logs hello -f shows what the plugin logs, and bb plugin list shows its status.
6. Change it
Section titled “6. Change it”Add a count command. In server.ts, add an entry to the commands list and a case to the switch inside run:
commands: [ /* … */ { name: "count", summary: "Count todos", usage: "bb hello count" } ],
case "count": { const todos = await listTodos(); return { exitCode: 0, stdout: String(todos.length) };}Save the file. bb plugin dev prints reloaded hello, and bb hello count prints the number. bb plugin logs hello now shows loaded from the new instance before disposed from the old one: the reload order from How a plugin works.
bb reads the commands list without running your code and adds bb hello count to the skill it generates for plugin commands. Two texts are yours to update by hand: the usage string in server.ts, which bb hello --help prints, and the command table in skills/example-todos/SKILL.md, the skill the agent loads for this plugin.
When something does not work
Section titled “When something does not work”| Symptom | Cause | Fix |
|---|---|---|
@get-bb/plugin-sdk does not resolve | the SDK types are not installed | run npm install --include=dev; inside the bb monorepo, pnpm exec turbo run build:types --filter=@get-bb/plugin-sdk |
| portals, toasts or focus behave oddly | a shimmed package such as React or radix is in dependencies, so a second copy is bundled | move every package listed in RUNTIME_SLOT_BY_SPECIFIER to devDependencies; zod stays in dependencies |
the plugin stays in needs-configuration | the plugin called bb.status.needsConfiguration(...), or a service failed with NeedsConfigurationError | fix the configuration, then run bb plugin reload hello |
| the SDK is older than the running bb | the scaffold pinned the SDK of the bb that created it | run bb plugin types; bb plugin types --check does the same check in CI without writing |
Where to go next
Section titled “Where to go next”- To decide where your own idea goes in the window or in the agent, read Choosing a surface.
- To store a secret, declare a setting with
secret: true; its value never reaches the app entry. Thebb.settingscard has the details. - To render a component inside the agent’s reply, see the
messageDirectiveslot. - To ask the user a question from the CLI or a tool, see
bb.uiand thependingInteractionslot, and the propose-and-confirm pattern in Trust model.