Documentation
From JSON API to home screen.
Everything you need to create trackers for any JSON API, write result formats, and pin live widgets to your home screen.
On this page
Result format
How to write a result format
The result format reads a JSON response and extracts exactly what you point at. The building blocks — dot-paths, index paths, templates with inline math, list projections, loops, conditions, one-line filters and value maps — can all be mixed in one format. Every example below is rendered by the same engine the widget uses, so what you see here is exactly what the widget shows.
The result format only reads a response. If the API needs a header, an API key or a POST body before it answers, set that up in Advanced Options.
Dot paths
walk into objects
Each segment after a dot steps one level deeper into a JSON object.
Response: {"foo":"bar","fii":"bir"}
foo → bar Response: {"data":{"foo":"bar"}}
data.foo → bar Indexes
reach into arrays
Arrays are addressed by position — starting at 0. Mix them freely with object keys.
Response: [{"name":"BTC"}]
0.name → BTC Response: {"results":[{"price":"64k"}]}
results.0.price → 64k Templates
compose your own output
Wrap any path in curly braces to embed values inside custom text. Use as many placeholders as you like — everything outside the braces stays untouched. Newlines written as \n and tabs written as \t are rendered as real line breaks and tab characters.
{data.city} → Berlin Price: ${data.price} → Price: $64231 {data.city}\n{data.temp}°C Berlin
21°COptional values
silent fields & defaults
A path that does not exist (or is null) normally renders —. Prefix the path with ? to render nothing instead, or follow it with ?: and a literal to show a fallback. An empty string and zero are real values and are never replaced.
Price: {?missing}ok → Price: ok A missing optional field leaves no trace.
{missing ?: "n/a"} → n/a Literal fallback text; the surrounding quotes are optional.
{foo ?: none} → bar A present value is kept as-is.
Inline math
transform numbers
Inside a placeholder, combine a path with + - * or / and a number. One operation per placeholder — chain complex math into your source instead.
{data.temp*1.8} → 37.80 Celsius → Fahrenheit-ish
{stats.bytes/1048576} MB → 12.50 MB Lists
project one field from every item
Put * in the path to collect one field from every item of an array — no loop block needed. The lines are joined with a newline between them only, never after the last one.
Response: {"stations":[{"brand":"Alpha","power":10},{"brand":"Bravo","power":20},{"brand":"Charlie","power":30}],"name":"Zync","count":3}
{stations[*].brand} Alpha
Bravo
Charlie{stations.*.brand} Alpha
Bravo
CharlieThe same thing — .* is shorthand for [*].
{big[*]} [1,2]
[3,4]Projects whole items; arrays and objects render as JSON.
Limiting items. A slice or count goes right after the array name, and works everywhere the array does:
{stations[0:5].brand} Alpha
Bravo
CharlieItems 0 to 4 (the end index is excluded).
{stations[-3:-1].brand} Alpha
BravoNegative indices count from the end.
{stations(5).brand} Alpha
Bravo
CharlieShorthand for the first 5 items.
{stations[2].brand} → Charlie A single item (the legacy stations.2.brand still works).
Zipped rows. When one line has two or more list fields, the whole line repeats once per item and the rows are joined by newlines between them — the fields stay lined up. The first list that resolves drives the row count; a shorter list shows — on the missing rows.
{stations.*.brand} {stations.*.power} € Alpha 10 €
Bravo 20 €
Charlie 30 €{stations(3).brand} {stations(3).power} Alpha 10
Bravo 20
Charlie 30A count works on every field of the row.
{name} · {stations.*.brand} {stations.*.power} Zync · Alpha 10
Zync · Bravo 20
Zync · Charlie 30Plain fields resolve once, against the whole response, and repeat on every row.
One list is not a zip. With a single list field there is nothing to line up, so the values keep the inline join. The same syntax works without braces too — a bare stations.*.brand is valid.
{stations.*.brand} Alpha
Bravo
CharlieLoops
a multi-field row per item
An #each <path> block repeats its body once per item. The header and #end sit on their own lines; every line between them is repeated. Inside the block, {...} resolve against the current item; outside, against the whole response. Rows are joined with a newline between them only, never after the last one.
#each stations
{brand} — {power} kW
#end Alpha — 10 kW
Bravo — 20 kW
Charlie — 30 kW- Same limiters as lists, written on the loop path: #each stations[0:5] slices, and #each stations(5) takes the first five. A slice needs both numbers — stations[2:] is not valid.
- Math works inside the body: {price * 2}.
- Align columns with a tab \t ({brand}\t{power}), or split the line into real columns with a bare |.
- Blocks can be nested, up to 8 levels deep.
- To place two loops one after another, start the next #each on a new line after #end — that newline is kept.
- A path that is missing, or is not an array, renders — instead of an error.
Conditions
show one branch or another
An #if block shows its body only when the condition holds; an optional #elseif and #else follow on their own lines, and #end closes the block. Without a match and without an #else, nothing is shown.
#if change > 0
▲ {change}
#else
▼ {change}
#end → ▲ 2.10 #if status == "open"
open now
#else
closed
#end → open now Compare against a value: quote strings, and write numbers, true, false or null bare.
#if count
has items
#else
empty
#end → empty A bare field is true unless it is missing, null, false, 0, empty text or an empty list.
#if change > 1
big
#elseif change > 0
up
#else
down
#end → big #elseif adds more choices; the first true condition wins.
- Operators: == != > < >= <=. Two numbers compare as numbers; anything else compares as text.
- #if blocks nest inside each other, inside #each, and can wrap one — up to 8 levels deep.
- #if, #elseif and #else each take a whole line; a stray or unclosed block shows an error instead of breaking the widget.
Filters & one-liners
skip rows, pick a value
Add where after an #each path to skip items whose condition is false — the row limit applies to what survives. For a single line, use ? : inside a placeholder instead of a whole block.
#each machines where status == "ok"
{name}
#end M1
M3Only the rows that pass the condition are rendered.
{change > 0 ? "▲ up" : "▼ down"} → ▲ up The same condition rules as #if; quote literal branch text.
{count ? "has items" : "empty"} → empty A bare condition tests truthiness.
- where needs spaces on both sides and works with slices too: #each stations(5) where power > 20.
- A branch is literal only when quoted; a bare word is a path, so {a ? x : y} reads the fields x and y.
Value maps
turn codes into labels
Put a | after a path and list key: label pairs. The value is matched against the keys (as text) and replaced by its label; a * key is the label for a missing or unmatched value. Without a * and no match, the placeholder — is shown.
{status | 1: Open, 2: Closed, *: Unknown} → Closed 2 matches the Closed label.
#each machines
{status | ok: ON, off: OFF}
#end ON
OFF
ONWorks inside loops and with text keys.
- Keys and labels are written bare; quote a label when it contains a comma or a leading or trailing space.
- An empty string is a value, not a miss, so it takes the * label unless it has its own empty key.
Decorations
bold, italic & code
The result text can be styled with three markers: *bold*, _italic_ and `code` (monospace). They read exactly the same in the widget, the preview and the share page.
*{symbol}* _{change}_ `updated {time}` → BTC -1.5% updated 09:41 - They combine in any order: *bold and _italic_*, _italic `then code`_.
- They work inside #each bodies and around {...} placeholders.
- Escape a marker with a backslash to print it literally: \*, \_, \` and \\.
- A span must open and close on the same line, cannot be empty, and cannot touch a space or tab at either edge — otherwise the markers stay literal.
- Doubled markers are not a special case: **bold** renders as written.
Columns
left, center, right
Every bare | outside {...} opens a column: the first cell sits on the leading edge, the last on the trailing edge, and everything between them is centered. Numbers line up across rows, which is what a price or score list needs.
#each stations
{brand} | {e5} €
#end HEM2.12 €PS-Laden2.26 €ARAL2.19 €#each stations
{brand} | {e5} € | {e10} €
#end HEM2.12 €2.31 €PS-Laden2.26 €2.42 €ARAL2.19 €2.35 €- Columns share the width evenly and every middle column centers its text, so {a} | {b} | {c} | {d} shows a left, b and c centered, d right.
- The layout reserves up to five columns (three centered); a sixth cell and beyond join the last centered column. One centered cell anywhere switches the whole result to that grid, so every row shares the column boundaries.
- A line without a | stays a full-width line, so a loop can mix a heading with column rows.
- The pipe inside an expression is untouched: {status | 1: Open} is still a map.
- Write \| to print a literal pipe.
- The widget shows up to 8 rows, then +N more; the app preview and the share page show every row.
Alignment
left, center, right
Columns default to first left, middle center, last right, and each column aligns the text inside its own cell. Put #align on the first line to change that; the line itself is never shown.
#align end start end
#each stations
{brand} | {e5} € | {e10} €
#end HEM2.12 €2.31 €PS-Laden2.26 €2.42 €ARAL2.19 €2.35 €- One token sets every column (#align center), two set the first and last (#align end start), three set first, middle and last (#align start center end).
- Words are start / left, center / middle, end / right, or the single letters s, c, e.
- It also works without columns: with one value the first token aligns the whole result (see below).
A leading marker inside a cell aligns just that cell: > end, ^ center, < start. Write them inside the field — {>price} — and a marker beats the directive.
#each stations
{>brand} | {e5} €
#end HEM2.12 €PS-Laden2.26 €ARAL2.19 €#align center
{stations.0.e5} € 2.12 €- The marker must sit at the start of the cell with no space after it, so > 5 stays literal. Anything else that starts with a marker needs an escape.
- A custom alignment uses evenly spaced columns; the default (first left, last right) keeps the compact row where the label takes the remaining width.
- Escape a literal marker with a backslash: \>, \<, \^.
- The app, the widget and the share page all read the same markers, so a shared snapshot keeps its alignment; the plain-text share drops them.
Fallbacks & edge cases
- Press TEST to inspect the raw response while you explore an unfamiliar source.
- Path doesn't exist or is null → the widget shows — instead of breaking.
- Invalid JSON → the error message is displayed so you can debug quickly.
- A list or loop emits at most 20 items, then ... (+N more); the whole result is capped at 2000 characters.
- A GET request never sends a body — use Advanced Options to POST one.
- Malformed loop syntax (a stray or unclosed #each) shows Error: Check your loop syntax..