bb Plugin API 2.0
This page is the front door to the 2.0 API. It shows the model and one complete plugin.
Use this when
- Start a new plugin. Learn the package shape, contract rules, and tier factories.
- Replace a bb feature. See how claims, winners, forks, and
Originalwork. - Share behavior. Publish a typed service or an exact module export.
- Choose the next page. Use the reading map to find reference material or a tutorial.
The vision
Almost everything in bb is a plugin. The layout, threads, files, Git, providers, agents, commands, and shared UI use the ordinary plugin path.
You can fork any plugin that supplies its source. bb supplies all first-party plugin source. A fork gets a new plugin ID and records its parent revision.
The user makes conflicts explicit. Each replaceable contract joins one global winner picker. A winner change remounts a surface or changes a service provider.
Plugins are fully trusted. App code shares the page, React, and the DOM. Server code shares the bb server process. There is no sandbox.
The kernel stays small. It owns boot, recovery, safe mode, plugin management, and stable ports. The ports are bb.storage, bb.secrets, bb.preferences, bb.realtime, bb.http, bb.rpc, and bb.plugins.
The SDK package is @get-bb/plugin. It supplies the /app, /server, /host, and /testing entries.
The contract model in one screen
A contract is a named, typed, versioned capability. A surface is an app contract. A service is an app or server contract.
Contract IDs use dots. First-party IDs start with bb.. Other IDs start with the plugin ID, such as acme.counter.service.
| Part | Meaning |
|---|---|
single |
One implementation is active. A replaceable contract has one global winner. |
list |
All claims are active in a stable order. The user can hide or reorder items. |
keyed |
One implementation is active for each key. A replaceable key has one global winner. |
chain |
Ordered members wrap or intercept one base operation. |
| Claim | A manifest row states that the plugin implements an existing contract. |
| Declaration | A surfaces or services row creates a contract that the plugin owns. |
| Winner | The global selection for a replaceable single contract or keyed key. |
Original |
The typed first-party default component for a winning single surface. Backend services do not receive it. |
| Service | A token resolves to the current live in-process object. A replaceable service names a default provider. |
| Edge | requires, optional, and watched state how a plugin follows another service. |
A required consumer starts after its provider. It restarts when that provider changes. An optional consumer stays active when the provider disappears.
A watched consumer also stays active. It receives each provider change. The loader stages all factory work and commits one complete plugin generation.
The source manifest is bb.plugin.jsonc. It contains claims, declarations, edges, exports, and artifact paths.
The build writes contract.json. That file contains the full schemas behind the short manifest rows. Do not edit it.
Use a contract when code must follow the current winner. Use a module export when code needs one exact implementation.
Build a counter plugin
This plugin owns one counter service. It claims the counter CLI verb and one status bar item.
The server uses private plugin storage. The app uses a typed RPC contract to reach the server.
bb.plugin.jsonc
{
"$schema": "https://getbb.app/schemas/plugin-v2.schema.json",
"schemaVersion": 2,
"id": "acme.counter",
"version": "1.0.0",
"name": "Counter",
"description": "Keeps one shared counter.",
"category": "developer",
"engines": { "bb": "^2.0.0", "sdk": "^2.0.0" },
"claims": [
{ "service": "acme.counter.service", "version": "^1.0.0" },
{ "service": "bb.commands.cli", "version": "^1.0.0", "key": "counter" },
{ "surface": "bb.layout.statusbar", "version": "^1.0.0", "order": 50 }
],
"services": [
{
"id": "acme.counter.service",
"version": "1.0.0",
"kind": "single",
"replaceable": true,
"defaultProvider": "acme.counter",
"contract": "./src/contracts.ts#counterService",
"stability": "stable"
}
],
"surfaces": [],
"requires": [],
"optional": [],
"watched": [],
"exports": {
"./contracts": "./dist/contracts.mjs"
},
"artifacts": {
"app": "./dist/app.mjs",
"server": "./dist/server.mjs"
}
}The plugin declares its own service and claims it in the same manifest. The other two claims refer to contracts that first-party plugins own.
src/contracts.ts
import { defineService } from "@get-bb/plugin";
import { defineRpcContract } from "@get-bb/plugin/contracts";
import { z } from "zod";
export interface CounterValue {
value: number;
}
export interface CounterService {
get(): Promise<CounterValue>;
bump(by?: number): Promise<CounterValue>;
}
export const counterService = defineService<CounterService>(
"acme.counter.service",
"1.0",
);
const value = z.object({ value: z.number().int() });
export const counterRpc = defineRpcContract({
id: "acme.counter.rpc",
version: "1.0.0",
methods: {
get: { input: z.object({}), output: value },
bump: {
input: z.object({ by: z.number().int().positive().default(1) }),
output: value,
},
},
});The service token follows the current service winner. The RPC contract gives the browser a typed transport to this plugin's server.
src/server.ts
import { defineServerPlugin } from "@get-bb/plugin/server";
import { counterRpc, counterService } from "./contracts.js";
import type { CounterService } from "./contracts.js";
export default defineServerPlugin(async (api) => {
const store = api.storage.kv();
let current = (await store.get<number>("value")) ?? 0;
const counter: CounterService = {
async get() {
return { value: current };
},
async bump(by = 1) {
current += by;
await store.set("value", current);
return { value: current };
},
};
api.services.provide(counterService, counter);
api.cli.add({
name: "counter",
summary: "Read or change the counter",
usage: "bb counter [get|bump] [amount]",
commands: [
{ name: "get", summary: "Read the counter", usage: "bb counter get" },
{ name: "bump", summary: "Add to the counter", usage: "bb counter bump [amount]" },
],
async run(argv) {
if (argv[0] === "get") {
const result = await counter.get();
return { exitCode: 0, stdout: `${result.value}\n` };
}
if (argv[0] === "bump") {
const amount = argv[1] === undefined ? 1 : Number(argv[1]);
if (!Number.isInteger(amount) || amount < 1) {
return { exitCode: 2, stderr: "The amount must be a positive integer.\n" };
}
const result = await counter.bump(amount);
return { exitCode: 0, stdout: `${result.value}\n` };
}
return { exitCode: 2, stderr: "Use get or bump.\n" };
},
});
api.rpc.register(counterRpc, {
get: () => counter.get(),
bump: ({ by }) => counter.bump(by),
});
});The server factory receives a typed api object. Its service, CLI, and RPC registrations get automatic cleanup.
The factory stages each registration. The loader commits them only after the factory succeeds.
src/app.tsx
import { definePlugin, useRpc } from "@get-bb/plugin/app";
import { Button } from "@bb/ui";
import { useEffect, useState } from "react";
import { counterRpc } from "./contracts.js";
function CounterStatus() {
const rpc = useRpc(counterRpc);
const [count, setCount] = useState<number | null>(null);
useEffect(() => {
void rpc.call("get", {}).then((result) => setCount(result.value));
}, [rpc]);
async function bump() {
const result = await rpc.call("bump", { by: 1 });
setCount(result.value);
}
return (
<Button size="sm" variant="ghost" onClick={() => void bump()}>
Count {count ?? "…"}
</Button>
);
}
export default definePlugin((api) => {
api.surfaces.add("bb.layout.statusbar", {
id: "counter",
order: 50,
align: "end",
title: "Increase the counter",
component: CounterStatus,
});
});The app factory supplies the status bar claim. The Layout plugin renders all active list items in a stable order.
Build and run the plugin:
bb plugin build .
bb plugin dev .
bb counter get
bb counter bump 2The build validates bb.plugin.jsonc and the generated contract.json as one unit. A missing claim or implementation stops the build.
The first-party plugin roster
The kernel is not a plugin. See the kernel reference for its loader, ports, and recovery work.
Seventeen core plugins use the same manifest, claims, factories, and lifecycle as external plugins.
| Plugin | ID | Main responsibility | Reference |
|---|---|---|---|
| Layout | bb.layout |
App shell, layout surfaces, themes, and status items | Layout |
| Threads | bb.threads |
Headless thread records, execution, timeline data, drafts, and selection | Threads |
| Thread UI | bb.thread-ui |
Thread list, view, composer, timeline, and side panels | Thread UI |
| Workspace | bb.workspace |
Headless projects, environments, routing, and environment providers | Workspace |
| Workspace UI | bb.workspace-ui |
Project switcher and environment pages | Workspace UI |
| Files | bb.files |
Headless file read, write, watch, and upload services | Files |
| Files UI | bb.files-ui |
File openers, source views, diff views, and previews | Files UI |
| Version control | bb.vcs |
Headless generic status, changed files, commits, heads, history, and diffs | Version control |
| Version control UI | bb.vcs-ui |
Commit controls, changes, head status, and history views | Version control UI |
| Git provider | bb.git |
The default bb.vcs provider and the Git-specific services |
Git provider |
| Providers | bb.providers |
Provider registry, bridge protocol, health, and host roles | Providers |
| Agents | bb.agents |
Agent tools, AI services, skills, and tool-call chains | Agents |
| Commands | bb.commands |
Headless commands, keybindings, interceptors, and CLI verbs | Commands |
| Palette | bb.commands-ui |
The default replaceable command palette | Palette |
| Settings | bb.settings |
Settings pages, schema forms, and secrets UI | Settings |
| Mentions | bb.mentions |
Mention providers, search, resolution, and composer use | Mentions |
| UI kit | bb.ui |
Exact shared components with no claims | UI kit |
The provider plugins also ship as ordinary built-ins. They are bb.claude-code, bb.codex, bb.pi, and bb.acp.
Reading map
- Use the 2.0 design pages as the API reference. Start with the 2.0 overview, then read authoring.
- Use the Guide for tutorials. Start with Getting started and follow the task-based lessons.
- Use the 1.0 reference as an archive. Open How to read the 1.0 reference only for old plugins and migration work.
The design reference defines the 2.0 contract. The tutorials can lag while the implementation moves to that contract.