Reference
Manifest reference
Every table and key in slowapp.toml for contract 3, and the rules the checker enforces across keys.
slowapp.toml sits at the root of an app’s repository and is copied into the image at /app/slowapp.toml. It names the app, declares its grants, jobs and feeds, and lists the examples used for test renders.
- Check it with
python3 -m slowapp_sdk validate(Python SDK reference). - The schema is
sdk/slowapp_sdk/schema/slowapp.schema.jsonin slowapp-base. The rules a schema can’t express are listed below. - Unknown keys are errors in the checker, so a typo can’t silently drop a grant. The hub ignores unknown keys, so a newer manifest still loads on an older hub.
Example: Reminders
From slowapp-reminders: questions on the screen, no network.
[app]
id = "reminders"
title = "Reminders"
contract = 3
image = "ghcr.io/mike-anderson/slowapp-reminders"
memory = "48m"
tmp = "8m"
timeout_s = 30
network = "none"
prompts = true
imports = ["legacy:reminders"] # P4 transitional: reminders.json, owned by the legacy page
[[job]]
name = "render"
expected_s = 3
daily = "00:01"
[[feed]]
name = "prompts"
size = "slot"
[[example]]
name = "due"
caption = "6 Oct 2026 10:00: watering due, NexGuard re-asks at 18:00, the vet at 15:00"
fixture = "fixtures/two-due"
now = "2026-10-06T10:00:00-07:00"
feeds = { prompts = "452x444.k4" }
The Weather tutorial shows a networked app’s manifest, with storage and three feeds.
[app]
| Key | Type | Meaning |
|---|---|---|
id | [a-z][a-z0-9-]{1,23} | The app’s name everywhere: in the layout (<id>/<feed>), on the hub, in paths. The repository is slowapp-<id> by convention. |
title | text, up to 40 characters | Shown on the hub’s app card. |
contract | 3 | The contract version. A hub refuses any other. |
image | text | The image’s registry path, without a tag. The hub pins the exact digest when the app is installed. |
memory | size (48m, 1g) | Hard memory limit. tmp and cache count against it. |
tmp | size | Size of /tmp, a scratch folder in memory. |
timeout_s | 1–3600 | The job is stopped after this many seconds (exit 137). |
network | none or egress | egress allows outgoing connections, to the internet and to your home network, but never to the hub’s own machine. |
secrets | list of names | Secrets the app needs, as files at /run/secrets/<name>. If one is missing the job ends with “needs config” (exit 5). |
optional_secrets | list of names | Secrets given to the app if they are set. |
data | { max_mb = N } | A private /data folder that survives between runs. The hub pauses the app if it grows past max_mb. |
cache | { tmpfs_mb = N } | A /cache folder in memory, emptied when the container stops. |
prompts | true or false | The questions grant: the app may put questions on the screen. Needs a feed named prompts. |
exports, imports | lists | Transitional, for apps moving out of the older system. New apps don’t use them. |
[settings]
Each key ([a-z][a-z0-9_]{0,31}) is a setting. The hub shows it in a form and gives the values to the app in /in/settings.json; in Python, ctx.settings fills in the defaults. See Settings, schedules and pages.
| Field | Meaning |
|---|---|
type | string, number, integer, boolean or choice |
label, help | Text for the form |
optional | May be left empty |
default | Default value |
min, max | Bounds for numbers |
choices | The allowed values of a choice |
[settings]
tz_override = { type = "string", optional = true }
lookahead_days = { type = "number", default = 14, min = 1, max = 60 }
[ui]
Only for apps with a web page. See Settings, schedules and pages.
| Key | Meaning |
|---|---|
idle_s | 10–3600. Stop the page’s container after this long with no viewer and no job. |
start_timeout_s | 1–120. How long serve may take to start answering. |
[events]
[events]
accept = ["answer"]
The kinds of answer payload (emit) this app receives. Needs a job named event. See Questions and answers.
[[job]]
| Key | Meaning |
|---|---|
name | [a-z][a-z0-9_-]*. render draws the feeds and is run when a screen needs cards; event receives answers. |
every | A schedule such as 15m or 1h, at least 5 minutes. |
daily | HH:MM, local time. Not together with every. |
jitter_s | A random delay added to a schedule. |
inputs | self:<export> or <app>:<export>: skip the job while these inputs haven’t changed. |
expected_s | How long the job usually takes, used until the hub has measured it. |
Every job can also be run by hand.
[[feed]]
| Key | Meaning |
|---|---|
name | The feed name; the layout refers to it as <app>/<name>. |
size | "slot": the size of whatever slot shows the feed, passed to the app on each run. "WxH" fixes the size. |
[[example]]
A test render. The checking tools and CI render every example and check the output.
| Key | Meaning |
|---|---|
name | Example name; previews are named <app>-<example>-<feed>-<card>.png. |
fixture | fixtures/<folder>: the test data, given to the app as a read-only /data. |
now | The fixed time, RFC 3339 with an offset. |
feeds | Which feeds to draw, at which surface: { prompts = "452x444.k4" }. |
job | The job to run. Default render. |
settings | Settings for this example. |
tz | Time zone for this example. |
caption | A caption for the preview. |
[ci]
Read only by CI; the hub ignores it. depends is kept for compatibility.
Cross-field rules
The checker enforces:
- Job, feed and example names are unique.
- An app with feeds has a
renderjob;[events] acceptneeds aneventjob. prompts = trueif and only if there is a feed namedprompts.- A secret is either required or optional, not both.
everyanddailyexclude each other;jitter_sneeds one of them;everyis at least 5 minutes.tmppluscache.tmpfs_mbis less thanmemory.- Setting defaults have the right type and sit within
min,maxandchoices. - Each example’s fixture folder exists, its job and feeds are declared, and a fixed-size feed matches the example’s surface.
It warns, without failing, about a slot surface other than 452 or 456 × 444, a feed with no example, and a required setting with no default.