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 Original work.
  • 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 2

The 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

The design reference defines the 2.0 contract. The tutorials can lag while the implementation moves to that contract.