Tutorial
Quickstart
Write a slowapp that draws one card, check it, and see it as it would look on the screen. About fifteen minutes, no Slowboard needed.
You will write a slowapp of three small files, build it, and render it the way a hub would. At the end you have a PNG of your card and a preview of the whole screen with your card in it.

The card you will make.
What you need
- Docker, or Podman.
- Python 3.
- A terminal. Everything below runs on Linux and macOS.
You don’t need a Slowboard or a hub. The tools check your app the same way a hub does.
1. Write the app
Make a folder called slowapp-hello with these files.
hello.py draws the card:
from datetime import timedelta
from PIL import ImageDraw
import slowapp_kit as kit
from slowapp_kit import BLACK, RED, Card
@kit.job("render")
def render(ctx):
surface = ctx.feed("hello")
if surface is None: # the hub didn't ask for this feed on this run
return
img = ctx.canvas(surface) # a white image exactly the size of the card
d = ImageDraw.Draw(img)
d.fontmode = "1" # no antialiasing: every pixel must be one of the four inks
cx, cy = surface.w // 2, surface.h // 2
d.text((cx, cy), "Hello", font=kit.font("DejaVuSans-Bold.ttf", 64), fill=BLACK, anchor="mm")
d.text((cx, cy + 60), f"{ctx.for_:%H:%M}", font=kit.font("DejaVuSans.ttf", 28), fill=RED, anchor="mm")
ctx.write_feed("hello", [Card("home", img, "Hello")],
valid_from=ctx.for_, valid_until=ctx.for_ + timedelta(hours=1))
kit.main()
slowapp.toml is the manifest. It names the app, says what it may use, and lists one test render (an example):
[app]
id = "hello"
title = "Hello"
contract = 3
image = "ghcr.io/you/slowapp-hello"
memory = "48m"
tmp = "8m"
timeout_s = 30
network = "none"
[[job]]
name = "render"
expected_s = 2
[[feed]]
name = "hello"
size = "slot"
[[example]]
name = "default"
fixture = "fixtures/empty"
now = "2026-10-07T09:00:00-07:00"
feeds = { hello = "452x444.k4" }
Dockerfile packages the app on the Python base image:
FROM ghcr.io/mike-anderson/slowapp-python:1.2.0@sha256:5cc196d93d9960643d51eac7a49f46fe4eb9a1c4f68deba0f60579ea9559a532
COPY slowapp.toml /app/slowapp.toml
COPY hello.py /opt/app/hello.py
ENV PYTHONPATH=/opt/app:/opt/slowapp
ENTRYPOINT ["python3", "/opt/app/hello.py"]
CMD ["job", "render"]
Finally, the example needs a folder of test data. This app reads no data, so the folder is empty:
mkdir -p fixtures/empty && touch fixtures/empty/.keep
2. Build it
docker build -t slowapp-hello:dev .
3. Get the checking tools
The tools that check an app, the SDK, ship inside the base image. Copy them out once, and give them a Python with Pillow:
docker create --name sdk ghcr.io/mike-anderson/slowapp-python:1 && docker cp sdk:/opt/slowapp /tmp/slowapp && docker rm sdk
python3 -m venv ~/.slowapp-venv && ~/.slowapp-venv/bin/pip install pillow
4. Check the manifest
PYTHONPATH=/tmp/slowapp ~/.slowapp-venv/bin/python -m slowapp_sdk validate
ok slowapp-hello
5. Render it
PYTHONPATH=/tmp/slowapp ~/.slowapp-venv/bin/python -m slowapp_sdk fixture --image slowapp-hello:dev --repeat
ok default 0.2 s hello: 1 cards
hello/home: aa190c154af92fa00463fd585e9200394df40f1bcdc36c8a418a270f00a1dbb9
1/1 examples passed; previews in out/previews
This ran your container the way a hub would, with no network, at the example’s fixed time (09:00), and checked everything it wrote. --repeat ran it twice and compared the results: the same input must always give the same picture. The long number is the card’s id, a hash of its pixels.
6. Look at it
Open out/previews/:
hello-default-hello-home.pngis your card.hello-default-frame.pngis the whole screen, with your card in the first slot of the Slowboard 1085’s default layout.

When it goes wrong
Delete the d.fontmode = "1" line, rebuild, and render again:
FAIL default exit 64 (contract error)
hello/render: exit 64: hello/home: 300 colours are not inks, e.g. [(1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4)]
Antialiased text has grey edges, and the screen has only four inks: black, white, yellow and red. The library refused to write the card, so the mistake shows up on your computer instead of on a screen.
A typo in the manifest is caught by validate:
error slowapp-hello: $.app: unknown key 'netwrok'
FAIL slowapp-hello
Next
- The smallest slowapp, explained goes through these files line by line.
- How slowapps work explains cards, feeds and jobs.
- Questions and answers: the Reminders app is the next tutorial.
- To start a real app, use the template repository: it has the CI workflow and tests set up.