Tutorial
Slowapps in any language
A slowapp is any container that reads a few environment variables and writes PNGs and a JSON file. Here is one in Go, with no Slowboard library at all.
The Python SDK is a convenience. The hub only cares what your container reads and writes, which is set out in the container contract. This page builds a slowapp in Go that draws one card, a red square, and passes the same checks as a Python app.

What the container has to do
For a render job, the hub starts your image with the command job render and:
- sets
SLOWAPP_FEEDSto a JSON list of the feeds it wants, each with a widthwand heighth; - sets
SLOWAPP_FORto the time the cards will be shown; - expects, for each feed,
/out/feeds/<feed>/deck.jsonand one PNG per card in/out/feeds/<feed>/cards/; - reads your exit code: 0 means “store what I wrote”.
Each card PNG must be exactly the requested size and indexed, with a palette that contains only the four inks: black, white, yellow, red. The contract has the full rules.
main.go
// A slowapp in Go: one card with a red square, written straight to /out.
package main
import (
"encoding/json"
"fmt"
"image"
"image/color"
"image/png"
"os"
"path/filepath"
"time"
)
// The four inks, in this order. A card's palette may hold only these colours.
var inks = color.Palette{
color.RGBA{0, 0, 0, 255}, // 0 black
color.RGBA{255, 255, 255, 255}, // 1 white
color.RGBA{255, 255, 0, 255}, // 2 yellow
color.RGBA{255, 0, 0, 255}, // 3 red
}
// One entry of SLOWAPP_FEEDS: a feed the hub wants drawn, and its size.
type feed struct {
Feed string `json:"feed"`
W int `json:"w"`
H int `json:"h"`
}
func fail(code int, msg ...any) {
fmt.Fprintln(os.Stderr, msg...)
os.Exit(code)
}
func main() {
if len(os.Args) != 3 || os.Args[1] != "job" || os.Args[2] != "render" {
fail(64, "usage: job render")
}
var feeds []feed
if err := json.Unmarshal([]byte(os.Getenv("SLOWAPP_FEEDS")), &feeds); err != nil {
fail(64, "SLOWAPP_FEEDS:", err)
}
t, err := time.Parse(time.RFC3339, os.Getenv("SLOWAPP_FOR"))
if err != nil {
fail(64, "SLOWAPP_FOR:", err)
}
for _, f := range feeds {
if f.Feed != "square" {
continue
}
img := image.NewPaletted(image.Rect(0, 0, f.W, f.H), inks)
for i := range img.Pix {
img.Pix[i] = 1 // white
}
s := f.H / 3
for y := f.H/2 - s/2; y < f.H/2+s/2; y++ {
for x := f.W/2 - s/2; x < f.W/2+s/2; x++ {
img.SetColorIndex(x, y, 3) // red
}
}
dir := filepath.Join("/out/feeds", f.Feed)
if err := os.MkdirAll(filepath.Join(dir, "cards"), 0o755); err != nil {
fail(1, err)
}
out, err := os.Create(filepath.Join(dir, "cards", "home.png"))
if err != nil {
fail(1, err)
}
if err := png.Encode(out, img); err != nil {
fail(1, err)
}
out.Close()
deck, _ := json.Marshal(map[string]any{
"cards": []map[string]string{{"id": "home", "title": "A red square", "kind": "home"}},
"valid_from": t.Format(time.RFC3339),
"valid_until": t.Add(time.Hour).Format(time.RFC3339),
})
if err := os.WriteFile(filepath.Join(dir, "deck.json"), deck, 0o644); err != nil {
fail(1, err)
}
}
}
Go’s PNG encoder writes a Paletted image with a four-colour palette as a 2-bit indexed PNG, which is exactly what the contract asks for.
Dockerfile
FROM golang:1.23 AS build
WORKDIR /src
COPY main.go .
RUN CGO_ENABLED=0 go mod init square && go build -o /square .
FROM scratch
COPY slowapp.toml /app/slowapp.toml
COPY --from=build /square /square
ENTRYPOINT ["/square"]
CMD ["job", "render"]
The final image holds the binary and the manifest, and nothing else.
slowapp.toml
[app]
id = "square"
title = "Square"
contract = 3
image = "ghcr.io/you/slowapp-square"
memory = "32m"
tmp = "4m"
timeout_s = 30
network = "none"
[[job]]
name = "render"
expected_s = 1
[[feed]]
name = "square"
size = "slot"
[[example]]
name = "default"
fixture = "fixtures/empty"
now = "2026-10-07T09:00:00-07:00"
feeds = { square = "452x444.k4" }
Check it
The SDK’s checks don’t care what language the image is in. With the tools set up as in the quickstart:
mkdir -p fixtures/empty && touch fixtures/empty/.keep
docker build -t slowapp-square:dev .
PYTHONPATH=/tmp/slowapp ~/.slowapp-venv/bin/python -m slowapp_sdk validate
PYTHONPATH=/tmp/slowapp ~/.slowapp-venv/bin/python -m slowapp_sdk fixture --image slowapp-square:dev --repeat
ok default 0.1 s square: 1 cards
square/home: bbadc33bbda906fb98a85d8468ebb3a59e4c502b5b1f2e436161bb87fb03c100
1/1 examples passed; previews in out/previews
What this app leaves out
The Python SDK handles several things this app skips. In another language you write them yourself, following the contract:
- Exit codes. Exit 4 when a fetch fails (the hub retries later), 5 when a setting or secret is missing, 3 when nothing changed since the last run.
- Fixture mode. In a test render
SLOWAPP_MODEisfixture, there is no network, and test data is in/data. - Secrets are files under
/run/secrets/. - The hub’s app card. Any job may write
/out/summary.jsonwith a status line for the hub’s web page.