TOML cheat sheet
Recipes and plans are TOML, and nothing else. The whole format reduces to four constructs β this page is those four, the shape they build, and the mistakes worth knowing about.
Read this first
A misspelled field name does not stop an apply. Keystone ignores keys it does
not recognise, on purpose: one recipe is published to many agents, and an agent
older than a field has to run the recipe rather than refuse it. The cost is that
restart_polciy = "never" sets nothing, and the component falls back to the
default β always.
So the agent reports what it ignored instead of hiding it, and a dry run refuses it outright. Run one before you trust a file you have edited by hand:
The four constructs
1. A table is a section
Square brackets open a section. Every key below it belongs to that section, until the next header.
2. A dotted name is nesting
[lifecycle.run.exec] is not a table called “lifecycle.run.exec”. It is exec
inside run inside lifecycle. The dots are the tree.
Nothing is indented in TOML β the header names the depth. These two are the same document:
3. Double brackets repeat
[[artifacts]] is an array of tables: write the header again and you add
another element. Four tables in Keystone repeat this way β [[artifacts]],
[[datasets]], [[dependencies]] and, in a plan, [[components]].
Both halves of that are enforced. Single brackets where an array belongs fails
with toml: cannot store a table in a slice, and repeating a single-bracket
table fails with toml: table metadata already exists β TOML refuses to define
the same table twice.
4. Values
| You want | Write | Used by |
|---|---|---|
| Text | name = "api" | almost everything |
Text with backslashes or \n left alone | path = 'C:\logs' (single quotes) | Windows paths, regexes |
| Several lines | script = """ β¦ """ (see below) | install and shutdown scripts |
| A list | args = ["-c", "config.yaml"] | args, capabilities |
| True or false | unpack = true | unpack, privileged, no_new_privileges |
| A number | failure_threshold = 3 | thresholds, ports, limits |
| A duration | interval = "10s" β a string | interval, timeout |
| A set of key/value pairs | a nested table, below | env, headers, labels |
Key/value maps are just tables, so they get a header of their own:
Multi-line strings are what install hooks want. The opening """ should be
followed by a newline; the leading newline is discarded, the rest is kept
verbatim:
The shape of a recipe
One component: where to get it, how to install it, how to run it, how to know it
is alive, and what must be ready first. Every block below is optional except
[metadata] and [lifecycle.run].
[lifecycle.run.exec] and [lifecycle.run.container] are the two halves of one
choice: type = "process" uses the first, type = "container" the second. A
container recipe replaces the exec and security blocks with:
Note the last header: ports and mounts repeat, so they take double brackets
and keep the full dotted path.
Field-by-field types and defaults are in Recipe and plan schema; the prose walkthrough is in Recipes.
The shape of a plan
A plan is a list of names bound to recipes. That is all it is.
There is no ordering in a plan. Order comes from [[dependencies]] in the
recipes β see Dependencies.
Traps
An unknown key does not stop the apply
Keystone accepts a key it does not recognise. That is deliberate, and it is what
makes a fleet upgradeable: one recipe is published to many devices, and an agent
whose binary predates a field must still run the recipe rather than refuse it.
[artifacts.delta] reached existing fleets on exactly that property.
The cost is that a typo looks identical to a field from the future. The field you meant stays unset, and the default takes over:
An empty restart_policy resolves to always. The component you wanted dead
after one run restarts forever.
What the agent will not do is hide it. Every key it did not understand is named, with its line, in three places:
| Where | What happens |
|---|---|
| A dry run | Refuses the plan. This is the authoring path: you are one edit away from a fix and nothing is deployed yet |
| The agent’s log | WARNING ignoring recipe recipes/api.toml (component api): "lifecycle.run.restart_polciy" (line 6) |
GET /v1/plan/status | an unknownFields array, for an operator who is not reading stdout |
If unknownFields is non-empty on a device, either that agent is older than the
recipe β which is fine and expected mid-rollout β or somebody has a typo.
A key can land in the wrong table without complaint
A key belongs to the header above it. This is valid TOML, and it lands as an unknown field for the same reason the typo does:
Put scalars before the sub-table headers, or you will keep adding keys to the last table you opened rather than the one you meant. A dry run catches this one too.
What is rejected outright
A wrong value never gets the benefit of the doubt, at any stage β there is no fleet-compatibility argument for a value the schema already enumerates:
capabilities = [] is not the same as omitting it
Declaring the key β even empty β drops every capability from the bounding set.
Omitting it leaves capabilities untouched. nil and [] differ on purpose; see
Process privileges.
Versions are strings
version = "1.4.0" is a string. version = 1.4 is a float and is rejected;
version = 1.4.0 is not valid TOML at all.
Checking a file before you deploy it
A dry run parses the plan, loads every recipe, verifies signatures, enforces the schema and computes the reconcile plan β without installing or starting anything. It catches malformed TOML, bad types, invalid enum values, missing required fields, unresolvable dependencies and every key the agent does not recognise, which a real apply deliberately tolerates.
That last one is why a dry run is worth running on a file you have hand-edited even when you are sure of it: it is the only place a misspelled field is an error rather than a line in a log.
One caveat worth stating: a dry run is checked against the agent you ran it on. A recipe using a field newer than that agent’s binary will be rejected by it and accepted by a newer one β which is the same tolerance seen from the other end. Run the dry run against an agent at least as new as the recipe.