Skip to content

Screencast: writing a Podlibre plugin

A shooting script for two videos that build the same plugin: one by hand, one with an AI assistant. They are deliberately the same plugin, so a viewer can watch either and get the same thing, and watch both to see what the assistant actually changes.

What gets built. Guests — a plugin that records who was on an episode and puts them in the feed as podcast:person. It is small, it is something a podcaster actually wants, and it touches every part of the plugin API worth showing: a manifest, a shared field, a workflow step, a screen, and a command.

Before recording. Podlibre running from source, a show with two or three episodes in it, a terminal and an editor side by side, and the window at 1280×800 so the text is readable when the video is scaled down. Delete ~/.local/share/Podlibre/consent.json first, so the permission dialog appears rather than being remembered from a previous take.


Video 1 — By hand (about ten minutes)

1. What we are making · 0:00–0:45

On screen: Podlibre, an episode's workflow.

This is Podlibre. Everything you see here is a plugin — importing a feed, transcribing, publishing. In the next ten minutes we are going to write one.

It will record who was on an episode, and put them in the podcast feed where other apps can read them.

Scroll the workflow. Point at a step. Then open Plugins in the sidebar.

Here is the one plugin Podlibre ships so far. By the end, ours is next to it.

2. Start from the template · 0:45–1:45

On screen: Codeberg, podlibre-plugin-template.

Press Use this template. You get your own repository with a plugin in it that already works.

Clone it, rename the folder to guests, open it in the editor.

git clone https://codeberg.org/you/guests.git
cd guests

Show the file list and name the parts — five files, ten seconds each:

plugin.toml says what the plugin is. plugin.py is the code. views/ is what the screen looks like. locales/ is the text. tests/ is the tests. That is the whole thing.

3. The manifest · 1:45–4:00

Edit plugin.toml. Type it, do not paste — this is the part worth watching.

[plugin]
id      = "guests"
name    = "Guests"
version = "0.1.0"
api     = "1"
description = "Who was on this episode, and in the feed."

The id is also the folder name. Lowercase, underscores.

[permissions]
files = ["episode"]

This is the important line. We are asking to read and write inside the episode's folder, and nothing else. We are not asking for the network, so this plugin cannot reach the network — not "should not", cannot. There will be no ctx.net when we write the code.

[[activities]]
id    = "guests"
scope = "episode"
phase = "enrich"

That puts us in the workflow, in the enrich group, next to chapters and metadata. This is where people look for work to do, so it is where a plugin should be — not in a menu they have to remember.

[[surfaces]]
kind     = "screen"
activity = "guests"
view     = "views/main.toml"

[[fields]]
key    = "guests"
on     = "episode"
type   = "list"
shared = true
rss    = "podcast:person"

shared = true means any other plugin can read this. rss is where it goes when the episode is exported — so a publishing plugin written before ours existed will put our guests in the feed, without ever having heard of us.

4. The screen · 4:00–5:30

Edit views/main.toml.

[[widgets]]
kind = "heading"
text = "@screen.heading"

[[widgets]]
kind  = "input"
id    = "name"
label = "@screen.name_label"

[[widgets]]
kind    = "button"
label   = "@screen.add"
command = "guests.add"
variant = "primary"

[[widgets]]
kind = "list"
id   = "guests"

Notice what this is not. It is not code that draws anything. We are describing what we want and Podlibre builds it — which is why writing a plugin needs no knowledge of a toolkit, and why no plugin can freeze the window.

The @ means look this up in our own translations.

5. The code · 5:30–7:30

Edit plugin.py. Delete what is there; type this.

from podlibre import plugin


@plugin.command("guests.add")
def add(ctx, name=""):
    name = name.strip()
    if not name:
        return {"guests": ctx.episode.field(key="guests") or []}
    guests = list(ctx.episode.field(key="guests") or [])
    if name not in guests:
        guests.append(name)
    ctx.episode.set_field(key="guests", value=guests)
    return {"guests": guests}

Three things to point out.

ctx is everything we are allowed to touch. The episode is there because every plugin gets it. There is no ctx.net — try it and you get an error telling you which line of the manifest is missing.

The widget with id = "name" arrives as the name argument. That is the whole wiring.

And what we return fills widgets of the same name: we return guests, and the list called guests fills itself. We never say how.

6. The words · 7:30–8:00

Edit locales/en.po.

msgid "activity.guests.title"
msgstr "Guests"

msgid "screen.heading"
msgstr "Who was on this episode?"

msgid "screen.name_label"
msgstr "Name"

msgid "screen.add"
msgstr "Add"

Plain gettext. Podlibre reads .po directly, so there is no build step, and a translator can help you without touching Podlibre itself.

7. Run it · 8:00–9:30

podlibre --plugin-dir .

The permission dialog appears.

And there it is. Before our plugin runs, Podlibre tells the user what it asked for, in words: Your files — read and write inside this show's folder, and nowhere else. That list came straight out of the manifest.

Allow. Open an episode's workflow.

There is our step, in the enrich group. Click it.

Add two guests. Show them appear in the list.

Open Plugins in the sidebar.

And here we are, next to RSS Import, saying what we provide and what we may touch. Nothing special happened: our plugin used exactly the same API the built-in one does.

8. Tests, and the end · 9:30–10:00

PODLIBRE=../Podlibre python -m unittest discover -s tests

The template came with tests and they still pass, because a plugin is ordinary Python with no toolkit in it.

That is a plugin. Push it, tell people the address, and Podlibre will install it from there.


Video 2 — With an assistant (about six minutes)

Same plugin, same ending. The point of this one is not that it is faster — it is what you still have to do yourself.

1. Why the template has two markdown files · 0:00–1:00

On screen: the fresh clone, AGENT.md and CLAUDE.md open.

The template ships with these two. They are the plugin API, the manifest schema and the house rules, written for an assistant to read.

This is the difference between an assistant that writes a Podlibre plugin and one that writes something that looks like a Podlibre plugin. Without them it will invent an API that does not exist, confidently.

Scroll AGENT.md briefly — the ctx table, the permissions list.

2. Ask for it · 1:00–2:00

In the terminal, in the clone:

claude

Read AGENT.md, then make this plugin record who was on an episode and put them in the feed as podcast:person. It should have a screen with a name field and an Add button, and the guests should be a shared field so an exporter can read them. Ask for no permissions beyond the episode's own folder.

Let it work. Do not narrate the waiting — cut.

3. Read what it wrote · 2:00–4:00

This is the part of the video that matters. Go through its output in this order, out loud:

First, the manifest. Does it ask for anything it does not use? This is the list the user is shown before installing, so it is a promise, not a configuration detail. If the network is in there and the code never fetches anything, take it out.

Open plugin.toml. Check [permissions] against the code.

Second, the field. Is shared right? Is the rss tag one that exists? An assistant will cheerfully invent podcast:guests, which no feed reader has ever heard of.

Third, loops. If it wrote anything that runs over every episode, does it check t.cancelled? That is the only thing standing between a user and a process they cannot stop.

Fix whatever is wrong, on camera, saying why.

4. Run it · 4:00–5:30

podlibre --plugin-dir .
PODLIBRE=../Podlibre python -m unittest discover -s tests

Same two commands as before. A plugin that passes its tests and does not load has not been tested, so always do both.

Show the permission dialog, the workflow step, adding a guest.

5. What that was worth · 5:30–6:00

The assistant wrote the boilerplate and I checked three things: that it asked for no more than it needed, that the feed tag was real, and that nothing can run away with the machine.

It saved the typing. It did not save the reading — and the reading is the part that keeps a plugin honest.


Notes for the edit

  • Cut every wait. Cloning, installing and thinking are not content.
  • Keep the permission dialog on screen for a good three seconds in both videos. It is the one moment that explains the architecture without anybody having to describe it.
  • Do not fix mistakes silently. If a typo breaks the load, show the error — the errors name the plugin, the key and what was expected, and that is worth demonstrating.
  • Captions. The six interface languages mean the audience is not only English-speaking; burn in nothing, ship a subtitle file.