# An opinionated guide to plug development

**URL:** https://community.silverbullet.md/t/an-opinionated-guide-to-plug-development/4072
**Category:** Plug Development
**Created:** [April 21, 2026, 8:47pm UTC](https://community.silverbullet.md/t/an-opinionated-guide-to-plug-development/4072 "2026-04-21T20:47:30Z")
**Posts on this page:** 6
**Page:** 1

<div class="post-metadata">

### Author: ![MrMugame](https://community.silverbullet.md/user_avatar/community.silverbullet.md/mrmugame/32/358_2.png) [@MrMugame](https://community.silverbullet.md/u/MrMugame)
#### Post date: [April 21, 2026, 8:47pm UTC](https://community.silverbullet.md/t/an-opinionated-guide-to-plug-development/4072/1 "2026-04-21T20:47:30Z")

</div>

## What even is a plug?

A plug is the most powerful way to extend Silverbullet. You can use Javascript inside a [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) (in the browser) to do pretty much whatever you want.

## Requirements

Know stuff about the web, know JavaScript, know [Space Lua](https://silverbullet.md/Space%20Lua). Some Typescript is good.

## Your Environment

First, you are going to need a controlled environment to test your plug. The easiest is to just run the latest docker container. Alternatively - That’s what I do - you can have the Silverbullet [repository](https://github.com/silverbulletmd/silverbullet) cloned, this way you can quickly look up docs and code.

```bash
git clone https://github.com/silverbulletmd/silverbullet
cd silverbullet

# You'll need to install node/npm and go

make setup
make

# Generally using the default docs for testing is useful
./silverbullet website/

# You can now access it at http://localhost:3000/, just leave it running in the background

```

## The Boilerplate

I’m generally not a fan of templates and thus also not a fan of the [plug template](https://github.com/silverbulletmd/silverbullet-plug-template), so let’s start from scratch, it’s not that much.

```bash
# Decide on a name, ideally one that’s not already taken
mkdir mycoolplug
cd mycoolplug

# Use git, create a README, add a LICENSE
git init
touch README.md # TODO
touch LICENSE # TODO

```

Now, like any good javascript project, we will need a `package.json`.

```json
{
  "name": "mycoolplug",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "build": "npx plug-compile mycoolplug.plug.yaml"
  },
  "dependencies": {
    "@silverbulletmd/silverbullet": ">=2.5.3"
  }
}

```

Two things here:

1. The dependency on `@silverbulletmd/silverbullet`, which is a tiny wrapper to make interactions with Silverbullet easier. We’ll definitely need it later. It also provides the `plug-compile` command
2. `npx plug-compile mycoolplug.plug.yaml`, this invokes a build script to actually build and bundle your plug. It needs a `mycoolplug.plug.yaml` though, called a manifest, so let’s create that.

```yaml
name: mycoolplug
functions:
  sayHi:
    path: main.ts:sayHi
  sayBye:
    path: main.ts:sayBye

```

Name seems obvious, but what are functions? Before getting to that, let’s create this `main.ts` file referenced here. (You could also use Javascript, but I’m a big sucker for Typescript, so I’ll continue with that)

```typescript
export function sayHi(): void {
  // Be patient! We'll get to it
}

// Async and parameters work too. Careful though, only json serializable types can be passed.
export async function sayBye(person: string): Promise<void> {
  // I said be patient
}

```

So, duh, functions are just (javascript/typescript) functions and as we’ll see later, everything is just a function, you’ll always start with a function. And be careful here, I’m explicit about naming here, don’t confuse functions with any upcoming terms (e.g. syscalls, commands)!  
For your own sake, I would avoid doing any of the following.

```yaml
# PLEASE DON'T DO THIS
# ...
functions:
  # This should align with the function name
  sayAWarmGreetingToTheUser: 
    path: main.ts:sayHi
  sayBye:
    # I would keep all functions in a single (or at least few selected) typescript file(s).
    path: some/random/file/somewhere/lost/in/your/repo.ts:sayBye

```

## Writing some code

To avoid testing your patience any further, we will write some code now.

```typescript
import { editor, system } from "@silverbulletmd/silverbullet/syscalls";

export async function sayHi(): Promise<void> {
  const version = await system.getVersion();
  
  await editor.flashNotification(`Hiiii! You are running version ${version} of Silverbullet!`);
}

```

I think it’s very readable, but we’ll get to the in’s and out’s of what these functions are in a second, let’s first try the code. To do that, we’ll need to build and load the code.

```bash
# If you haven't already
npm install

# This uses the build command from the `package.json`
# I generally add a cp command to directly copy the file to the space folder of my running Silverbullet instance
npm run build && cp mycoolplug.plug.js WHEREEVER-YOUR-SB-IS/silverbullet/website

```

Now let’s reload SB and see if our plug is loaded. Generally it should autoload, if not, do a `Plugs: Reload` and/or refresh the page a couple of times.  
There isn’t a good way to know if it’s loaded (maybe in the future), sometimes logs in the browser console can indicate that, but I wouldn’t trust it.

## Running some code

We can now invoke the function we just wrote by using [`system.invokeFunction`](https://silverbullet.md/API/system#system.invokeFunction(name,%20...)) inside of Space Lua, so let’s try this. The easiest way to execute Space Lua on demand is using an expression.

```auto
${system.invokeFunction("mycoolplug.sayHi")}
...
${system.invokeFunction("mycoolplug.sayBye", "Bob")}

```

Now if you let the expression render, it will call the function and run our code. That’s a big milestone! Just one more thing, before we get to doing some actually useful stuff.

## Syscalls

This is an important concept and if you used Silverbullet for a while you have probably heard about it and most definitely interacted with it.  
All those functions you constantly use to do anything, e.g. the ones we used above [`system.getVersion`](https://silverbullet.md/API/system#system.getVersion()) and [`editor.flashNotification`](https://silverbullet.md/API/editor#editor.flashNotification(message,%20type,%20options?)), are _syscalls_.  
A few important points:

1. They are (/should be) grouped into namespaces, in this case `system` and `editor` respectively, which we imported in our typescript file, and by which the [documentation](https://silverbullet.md/API) is grouped. Your own syscalls (yes you can create those), will generally be namespaced under you plugs name, e.g. `mycoolplug.sayHi`.
2. Syscalls are always async (at least for the caller)! We will always have to await here even though it may not make sense. (This stems from the fact, that Silverbullet uses messages to talk to our WebWorker, which are asynchronous by nature). This is a big difference from Space Lua.
3. Syscalls are not typed. While it may seem like they are, these are just thin, typed wrappers for the most essential syscalls.
4. “Why do syscalls exist? We already have functions?” Syscalls are (generally) functions. Furthermore, syscalls are for functions, which you want users to be able to call from Space Lua or other plugs, i.e. in code. Functions are way more general, you may want to use those for other things.

Now that we have all the most basic things out of the way, I’ll start with different sections on specific things, that you don’t necessarily need to read in order.

## Your own syscalls

To create your own syscalls, just annotate a function inside your manifest.

```yaml
# ...
functions:
  sayHi: 
    path: main.ts:sayHi
    # It's good practice to use your plug name as a namespace
    syscall: "mycoolplug.sayHi"

```

Now you can call your syscall inside of Space Lua.

```auto
${mymycoolplug.sayHi()}

```

To call them inside a plug, do the following.

```typescript
import { syscall } from "@silverbulletmd/silverbullet/syscalls";

// ...
syscall("mymycoolplug.sayHi")
// ...

```

## Creating custom widgets

Widgets in space lua are just objects of a specific form

```js
{
  _isWidget: true,
  html: "<h1>This is a header<h1>"
}
// or
{
  _isWidget: true,
  markdown: "This is *markdown*"
}

```

You can easily return these from functions and if you make these functions syscalls, they are very easy to call from Space Lua.

(To be very exact you can also pass DOM nodes, but that’s not practically possible, because you don’t have access to the DOM Tree and you also cannot serialize DOM nodes)

## Commands

Commands are, well, commands, e.g. `System: Reload`, `Page: Delete`, etc. To create a command, just annotate the function.

```yaml
# ...
functions:
  sayHi: 
    path: main.ts:sayHi
    command:
      # It's good practice to use your plug as a namespace
      name: "MyCoolPlug: Say Hi"

      ## OPTIONAL

      # Puts the command higher in the list
      priority: 10

      # Keybind for Windows/Linux
      key: "Ctrl-x"
      # Keybind for MacOS
      mac: "Cmd-x"

      # Hide this command from the list
      hide: true
      # Can be "rw" or "ro", if it's "rw", the command is only available in read/write mode, not in read only mode
      requireMode: "rw"
      # Can be either "any", "page", "notpage" or any string. Specifies for which editor this command is visible, when working with document editors.
      requireEditor: ""

      # Whether to disable this keybind if vim mode is enabled
      disableInVim: false

```

## Events

You can dispatch and listen to events. This is as easy as annotating a function

```yaml
# ...
functions:
  sayHi: 
    path: main.ts:sayHi
    events:
      - "editor:init"

```

The [list of available events can be found here](https://silverbullet.md/Event).  
Dispatching events is done via the `event.dispatch` syscall (for some reason the typescript wrapper is called `events.dispatchEvent`).  
One very useful thing is that you can return data from event listeners, with the “fastest” result returned first from the dispatch syscall.

---

<div class="post-metadata">

### Author: ![costalfy](https://community.silverbullet.md/user_avatar/community.silverbullet.md/costalfy/32/1629_2.png) [@costalfy](https://community.silverbullet.md/u/costalfy)
#### Post date: [April 22, 2026, 4:33am UTC](https://community.silverbullet.md/t/an-opinionated-guide-to-plug-development/4072/2 "2026-04-22T04:33:12Z")

</div>

awesome, thank you for this tutorial.

---

<div class="post-metadata">

### Author: ![stainless](https://community.silverbullet.md/user_avatar/community.silverbullet.md/stainless/32/3391_2.png) [@stainless](https://community.silverbullet.md/u/stainless)
#### Post date: [April 23, 2026, 12:24am UTC](https://community.silverbullet.md/t/an-opinionated-guide-to-plug-development/4072/3 "2026-04-23T00:24:33Z")

</div>

I've gotten as far as getting silverbullet to build. I did have to install some prerequisites to do so. Note that I'm on WSL2 running Ubuntu-24.04, and I expect that was the reason why I needed to explicitly install these:

```auto
sudo apt update && sudo apt upgrade -y
sudo apt install -y git make golang
sudo curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt install -y nodejs
sudo npx playwright install --with-deps # this is only needed because make setup doesn't have the --with-deps option

```

---

<div class="post-metadata">

### Author: ![ryanwwest](https://community.silverbullet.md/user_avatar/community.silverbullet.md/ryanwwest/32/3526_2.png) [@ryanwwest](https://community.silverbullet.md/u/ryanwwest)
#### Post date: [May 20, 2026, 12:16am UTC](https://community.silverbullet.md/t/an-opinionated-guide-to-plug-development/4072/4 "2026-05-20T00:16:48Z")

</div>

How do you like to debug plugs? As in, breakpoints and such. Littering code with print statements is an option, but not ideal.

---

<div class="post-metadata">

### Author: ![MrMugame](https://community.silverbullet.md/user_avatar/community.silverbullet.md/mrmugame/32/358_2.png) [@MrMugame](https://community.silverbullet.md/u/MrMugame)
#### Post date: [May 23, 2026, 10:23pm UTC](https://community.silverbullet.md/t/an-opinionated-guide-to-plug-development/4072/5 "2026-05-23T22:23:17Z")

</div>

It's hard. The build tooling theoretically allows debug builds with maps and stuff (I think), but the last time I tried that it was broken, so I generally just gave up on it and use print statements. It's less of a headache.

---

<div class="post-metadata">

### Author: ![ryanwwest](https://community.silverbullet.md/user_avatar/community.silverbullet.md/ryanwwest/32/3526_2.png) [@ryanwwest](https://community.silverbullet.md/u/ryanwwest)
#### Post date: [May 23, 2026, 10:37pm UTC](https://community.silverbullet.md/t/an-opinionated-guide-to-plug-development/4072/6 "2026-05-23T22:37:40Z")

</div>

I've learned that Chrome DevTools has a debugger that lets me breakpoint, step through code, and inspect variable values. The editing step is still disconnected in VS Code, but it works decently well. It looks like there might be some way to connect devtools in Vs Code directly too.
