---
title: "Extending <code class=\"docutils literal notranslate\"><span class=\"pre\">pbl</span></code>"
canonical_url: https://developer.repebble.com/pebbleos/docs/pbl/extending/
last_updated: 2026-10-21
sdk_version: 4.33.1
---
# Extending <code class="docutils literal notranslate"><span class="pre">pbl</span></code>

Source: https://developer.repebble.com/pebbleos/docs/pbl/extending/

## Structure

The package lives in `tools/libs/pbl-cli/`, and is a small core plus one module per command:

```
pbl/app/main.pyfindstheworkspace,assemblesthecommands,dispatchespbl/command.pyPblCommand,thebaseclass everycommandderivesfrompbl/workspace.pythecheckout:pbl.yml,thelayout,theextensionspbl/build.pyabuilddirectoryandwhatcanbereadbackfrom itpbl/runners.pythebinaryrunners(openocd,sftool,nrfutil)pbl/commands/thecommandsthemselves
```

### Adding a command

Drop a module in `pbl/commands/` with a `PblCommand` subclass. Commands are discovered by scanning the package, so there is no registry to update; classes whose name starts with an underscore are treated as shared bases rather than commands.

```
from pbl.command importPblCommandclass Size(PblCommand):group="build"def \_\_init\_\_(self):super().\_\_init\_\_("size","Report the firmware's size")def do\_add\_parser(self,parser\_adder):parser=self.add\_subparser(parser\_adder)parser.add\_argument("--sections",action="store\_true")returnparserdef do\_run(self,args,unknown):build=self.build\_dir()returnself.run\_cmd(["arm-none-eabi-size",build.elf])
```

`self.build_dir()` returns the configured build, failing with a useful message when there is none; `self.run_cmd()` and `self.check_cmd()` run things from the workspace root and honor `--dry-run`; raising `CommandError` is how a command fails. `group` picks the section of the top-level help the command is listed under.

### Feeds

`pbl feed <feed>` writes simulated phone data into the emulator. Each feed is a module in `pbl/feeds/` with a `Feed` subclass, discovered the same way commands are: it names its subcommand, adds its own options in `add_arguments()` and writes its data in `run()` through the `Watch` it is given, which wraps the blob DB client, sends and receives raw protocol packets, and turns into a printer under `--dry-run`. A feed that has to answer the watch registers handlers with `watch.on()` and keeps `run()` going until interrupted.

```
from pbl.feeds importBlobDb,Feedclass Steps(Feed):name="steps"help="A day of step counts"def add\_arguments(self,parser):parser.add\_argument("--steps",type=int,default=8000)def run(self,args,watch,inf):watch.blobdb\_insert(BlobDb.HEALTH,key,value)inf(f"pushed {args.steps} steps")
```

### Extension commands

A command that should not live in the package is declared in `pbl.yml` instead. It is only imported when it is the command being run, and is listed under “extension commands” rather than in one of the groups:

```
commands:- file: tools/pbl\_commands/size.pycommands:- name: sizeclass: Sizehelp: Report the firmware's size
```

## Adding a runner

A board lists the [runners](https://developer.repebble.com/pebbleos/docs/pbl.md#pbl-runners) it supports in its manifest, first one first:

```
runners:- openocd- nrfutil
```

`pbl flash`, `reset`, `run`, `bork`, `openocd` and `debug` all dispatch to it. Each runner contributes its own options (e.g. `--tty` for sftool), so they show up on all of those commands. New runners are modules in `pbl/runners/`, registered in that package’s `RUNNERS` table, and declare the operations they support in `capabilities()`.

Generated from PebbleOS [3a7b71785](https://github.com/coredevices/pebbleos/commit/3a7b71785fd439666c4a8bc116f561140e8dec01). This page is maintained in the pebbleos repository: [docs/pbl/extending.md](https://github.com/coredevices/pebbleos/blob/3a7b71785fd439666c4a8bc116f561140e8dec01/docs/pbl/extending.md).
