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°C

Optional 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
Charlie

The 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
Charlie

Items 0 to 4 (the end index is excluded).

{stations[-3:-1].brand}
Alpha
Bravo

Negative indices count from the end.

{stations(5).brand}
Alpha
Bravo
Charlie

Shorthand 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 30

A count works on every field of the row.

{name} · {stations.*.brand} {stations.*.power}
Zync · Alpha 10
Zync · Bravo 20
Zync · Charlie 30

Plain 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
Charlie

Loops

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
M3

Only 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
ON

Works 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..