2847500 means nothing without a currency symbol and thousands separators. A decimal like 0.124 needs to be shown as 12.4%. An ISO date like 2025-01-15T14:30:00Z should read “January 15, 2025.”
Pipes handle this formatting. They’re the | in expressions like {{ revenue | currency }} — they take the value on the left, transform it, and return a display-ready result. In Python, pipes are method calls on Rx objects: revenue.currency() compiles to {{ revenue | currency }}.
Here’s a dashboard card that uses several pipes together:
revenue.currency() turns 2847500 into $2,847,500.00. (revenue / target).percent(1) divides first, then formats the result as a percentage with one decimal place. Each pipe method returns a new Rx, so they chain naturally — and the pipe operator has the lowest precedence in the expression grammar, so arithmetic always evaluates before formatting.
Number Formatting
| Pipe | Python | Protocol | Result |
|---|---|---|---|
currency | .currency() / .currency("EUR") | {{ x | currency }} / {{ x | currency:EUR }} | $1,234.00 / €1,234.00 |
percent | .percent() / .percent(1) | {{ x | percent }} / {{ x | percent:1 }} | 76% / 75.6% |
number | .number() / .number(2) | {{ x | number }} / {{ x | number:2 }} | 1,234 / 1,234.00 |
round | .round(2) | {{ x | round:2 }} | 3.14 |
abs | .abs() | {{ x | abs }} | 42 |
percent multiplies by 100 before formatting. A value of 0.756 becomes 75.6%, not 0.756%. This matches how most APIs and databases store percentages (as decimals), so it usually does what you want. number and currency produce locale-formatted output (en-US by default).
Date and Time
ISO date strings are precise but unreadable. The date and time pipes turn them into human-friendly formats:| Pipe | Argument | Example Output |
|---|---|---|
date | short | 1/15/2025 |
date | (default: medium) | Jan 15, 2025 |
date | long | January 15, 2025 |
time | 2:30 PM | |
datetime | Jan 15, 2025, 2:30 PM |
time pipe also accepts time-only strings like "14:30".
String Formatting
| Pipe | Argument | Description |
|---|---|---|
upper | Uppercase the string | |
lower | Lowercase the string | |
truncate | max length | Clamp to N characters, append ... if truncated |
pluralize | singular word (default item) | Returns the word as-is for count 1, appends s otherwise |
pluralize pairs well with length for labeling dynamic counts: Rx("items").length().pluralize("item") produces {{ items | length | pluralize:'item' }}, which renders as “1 item” or “3 items” depending on the array size.
Array Operations
When state contains a list of objects, you often need to summarize it — count the elements, extract a subset, or join names into a string. Array pipes handle this without any server logic: The key line istodos.rejectattr("done").length() — it filters out completed items, then counts what’s left. The renderer evaluates the entire chain at runtime, so the “remaining” count stays current as items are checked off.
| Pipe | Argument | Description |
|---|---|---|
length | Number of elements (also works on strings) | |
join | separator (default , ) | Join elements into a string |
first | First element | |
last | Last element | |
selectattr | attribute name | Keep items where the attribute is truthy |
rejectattr | attribute name | Remove items where the attribute is truthy |
Chaining
Pipes chain left to right — each pipe receives the output of the one before it. This is how you compose multiple transformations in sequence:| Python | Protocol |
|---|---|
name.lower().truncate(20) | {{ name | lower | truncate:20 }} |
todos.rejectattr("done").length() | {{ todos | rejectattr:'done' | length }} |
revenue.currency().upper() | {{ revenue | currency | upper }} |
Rx object, so you can keep building. The chain reads naturally left-to-right in both Python and the protocol.
Pipe Arguments
Some pipes take an argument that modifies their behavior. In the protocol, the argument follows a colon:{{ score | percent:1 }}. In Python, it’s a method parameter: .percent(1).
The argument is always a simple token — a number, a string, or an identifier. It cannot be a sub-expression.
| Python | Protocol | Effect |
|---|---|---|
.join(" - ") | {{ tags | join:' - ' }} | Join with - separator |
.currency("EUR") | {{ price | currency:EUR }} | Format as euros |
.default("Anonymous") | {{ name | default:Anonymous }} | Fallback value |
.truncate(20) | {{ bio | truncate:20 }} | Limit to 20 characters |
Default Values
A bare literal after| acts as a default value when the left side is null or undefined. In Python, use .default():
| Python | Protocol | Behavior |
|---|---|---|
name.default("Anonymous") | {{ name | 'Anonymous' }} | "Anonymous" if name is undefined |
count.default(0) | {{ count | 0 }} | 0 if count is undefined |
"" or 0 will not trigger the default. For broader falsy defaults (including empty strings, zero, and false), use the || operator instead: