Canonical page: https://docs.singlebase.io/templates

# Templates

Templates compute values when you write documents. Use them to combine fields, do math, format dates, generate IDs, and clean up text, all inside the request.

::: v-pre

## Overview

**Templates** are strings that contain `{{ ... }}` expressions. Singlebase renders them against the document being written, then saves the result.

**Where templates work:**
- `data` in [`data.insert`](/data.md#data-insert) and [`data.update`](/data.md#data-update)
- `insert_data` and `update_data` in [`data.upsert`](/data.md#data-upsert)

**Quick grammar:**

| Syntax | Meaning |
|---|---|
| `{{ ... }}` | Expression. The result is a string |
| `&:{{ ... }}` | Native expression. Keeps the type: number, boolean, array, object, or null |
| `{{ $function() }}` | Helper function call |
| `{{ $func1($func2(value)) }}` | Nested helper calls |
| `{{ value \| len }}` | Filter |
| `{{ $now() \| date_shift('2days') }}` | Helper call followed by a filter |

**Example:**
```json
{
  "operation": "data.insert",
  "payload": {
    "collection": "orders",
    "data": {
      "customer": "ada lovelace",
      "price": 12,
      "quantity": 3,
      "label": "{{ quantity }} x Notebook",
      "total": "&:{{ price * quantity }}",
      "customer_name": "{{ $titlecase(customer) }}",
      "order_ref": "{{ $uppercase($shortid()) }}",
      "due_on": "{{ $now() | date_shift('14days') | date_format('YYYY-MM-DD') }}"
    }
  }
}
```

**Saved document:**
```json
{
  "customer": "ada lovelace",
  "price": 12,
  "quantity": 3,
  "label": "3 x Notebook",
  "total": 36,
  "customer_name": "Ada Lovelace",
  "order_ref": "9F2C41AB",
  "due_on": "2026-10-17"
}
```

---

## Expression Syntax

### {{ }} - String Expression

Evaluates the expression and returns a **string**. You can mix text and several expressions in one value.

```json
{
  "greeting": "Hello {{ first_name }}!",
  "summary": "{{ title }} ({{ views }} views)"
}
```

**Characteristics:**
- Always returns a string
- Supports interpolation with surrounding text
- A missing field renders as an empty string

---

### &:{{ }} - Native Expression

Evaluates the expression and returns its **native type**.

```json
{
  "total": "&:{{ subtotal + tax }}",
  "is_large": "&:{{ total >= 100 }}",
  "tag_count": "&:{{ tags | len }}"
}
```

**Result:**
```json
{
  "total": 125.5,
  "is_large": true,
  "tag_count": 3
}
```

**Characteristics:**
- Returns a number, boolean, string, array, object, or null
- The whole value must be one expression: no surrounding text
- Keeps the key even when the result is null or empty

---

### $function() - Helper Calls

Call a [helper function](#helper-functions) with a `$` prefix inside any expression.

```json
{
  "slug": "{{ $slugify(title) }}",
  "id_hash": "{{ $sha256(email) }}",
  "created": "{{ $now() }}"
}
```

Helpers can be nested:

```json
{
  "code": "{{ $uppercase($substring($trim(name), 0, 3)) }}"
}
```

---

### | filter - Filters

Pipe a value through one or more [filters](#filters). Filters can follow a field or a helper call.

```json
{
  "name_upper": "{{ name | trim | upper }}",
  "tag_count": "&:{{ tags | len }}",
  "expires_at": "{{ $now() | date_shift('30days') }}",
  "tag_list": "{{ tags | join(', ') }}"
}
```

---

## Expressions

Inside `{{ ... }}` you can use:

| Kind | Example | Result |
|---|---|---|
| Field | `{{ title }}` | Value of `title` |
| Nested field | `{{ profile.city }}` | Value of `profile.city` |
| Array index | `{{ items[0].name }}` | First item's `name` |
| Math | `&:{{ price * quantity }}` | `36` |
| Comparison | `&:{{ total >= 100 }}` | `true` |
| Logic | `&:{{ in_stock and price < 50 }}` | `true` |
| String concat | `{{ first_name ~ ' ' ~ last_name }}` | `"Ada Lovelace"` |
| Literal | `&:{{ [1, 2, 3] }}` | `[1, 2, 3]` |

Math operators are `+`, `-`, `*`, `/`, and `//`, and they only accept numbers. `+` also joins strings. `%`, `**`, and division by zero are not allowed. Logic uses `and`, `or`, and `not`.

Templates run in a sandbox. Only `{{ ... }}` expressions are supported; `{% ... %}` blocks are not.

**Evaluation order:**
1. Plain values and [data operators](/data.md#data-operators) are applied.
2. Templates are rendered against the result.

So templates see the new values:

```javascript
// Existing: { "views": 3 }
{
  "views:$incr": 1,
  "views_label": "{{ views }} views"
}
// Result:   { "views": 4, "views_label": "4 views" }
```

---

## Helper Functions

Call helpers as `$name(...)`. Arguments can be fields, literals, or other helpers.

### Date and Time

| Helper | Description | Example | Result |
|---|---|---|---|
| `$now()` | Current UTC date and time, ISO 8601 | `{{ $now() }}` | `2026-10-03T12:00:00+00:00` |
| `$date(value)` | Normalize a date to ISO 8601. Defaults to now | `{{ $date(published) }}` | `2026-01-15T10:00:00+00:00` |
| `$date_format(date, format)` | Format a date. Alias `$formatdate` | `{{ $date_format(created, 'YYYY-MM-DD') }}` | `2026-01-15` |
| `$date_shift(date, shift)` | Shift a date. Alias `$shiftdate` | `{{ $date_shift($now(), '+1 week') }}` | One week from now |

**Formats:** `ISO`, `ISO_DATE`, `ISO_TIME`, `ISO_DATETIME`, or tokens such as `YYYY-MM-DD`, `MMM D, YYYY`, and `HH:mm`.

**Shifts:** Combine signed amounts and units: `2days`, `+3 hours`, `-1 week`, `1 day - 3 hours`, `+2.5hours -30min`. Units are `y`/`year`, `mo`/`month`, `w`/`week`, `d`/`day`, `h`/`hour`, `min`/`minute`, and `s`/`second`, singular or plural.

### IDs and Hashing

| Helper | Description | Example result |
|---|---|---|
| `$uuid()` | UUID v4 without dashes | `7918dbc69cdf440386816bbb4f0ec817` |
| `$uuid4()` | UUID v4 with dashes | `49059b8c-3932-4784-bbb9-2e02ed05ae01` |
| `$uuid7()` | Time-ordered UUID v7 | `01a100cb-f63d-7c03-855a-50e15703d3c5` |
| `$shortid()` | 8-character random ID | `32fae10d` |
| `$sha256(value)` | SHA-256 hex digest | `b5fc85e5...c41d72` |

### Strings

| Helper | Description | Example | Result |
|---|---|---|---|
| `$uppercase(text)` | Uppercase. Alias `$upper` | `{{ $uppercase('api') }}` | `API` |
| `$lowercase(text)` | Lowercase. Alias `$lower` | `{{ $lowercase('API') }}` | `api` |
| `$titlecase(text)` | Capitalize each word. Alias `$title` | `{{ $titlecase('ada lovelace') }}` | `Ada Lovelace` |
| `$capitalize(text)` | Capitalize the first letter | `{{ $capitalize('ada lovelace') }}` | `Ada lovelace` |
| `$trim(text)` | Remove surrounding whitespace | `{{ $trim('  hi  ') }}` | `hi` |
| `$slugify(text)` | URL-friendly slug | `{{ $slugify('Hello World') }}` | `hello-world` |
| `$substring(text, start, end)` | Slice a string | `{{ $substring('lovelace', 0, 4) }}` | `love` |
| `$replace(text, old, new)` | Replace text | `{{ $replace('v1', 'v', 'version ') }}` | `version 1` |
| `$concat(a, b, ...)` | Join values as text | `{{ $concat(first, ' ', last) }}` | `Ada Lovelace` |
| `$truncate(text, words, suffix)` | Keep the first N words | `{{ $truncate(bio, 3, '...') }}` | `Hello world this...` |
| `$strip_html(html)` | Remove HTML tags | `{{ $strip_html('<b>Hi</b>') }}` | `Hi` |
| `$maskstr(text, visible, char)` | Mask all but the last N characters. Defaults `4`, `*` | `{{ $maskstr(card, 4) }}` | `************1234` |
| `$count_words(text)` | Number of words | `&:{{ $count_words(body) }}` | `120` |
| `$contains(text, part)` | Text contains a substring | `&:{{ $contains(title, 'API') }}` | `true` |
| `$includes(value, item)` | String, array, or object contains an item | `&:{{ $includes(tags, 'api') }}` | `true` |
| `$starts_with(text, prefix)` | Starts with a prefix | `&:{{ $starts_with(sku, 'A-') }}` | `true` |
| `$ends_with(text, suffix)` | Ends with a suffix | `&:{{ $ends_with(email, '.edu') }}` | `false` |

### Numbers

| Helper | Description | Example | Result |
|---|---|---|---|
| `$incr(value, by)` | Add `by` (default `1`). Missing values start at `0` | `&:{{ $incr(score, 5) }}` | `15` |
| `$decr(value, by)` | Subtract `by` (default `1`) | `&:{{ $decr(stock) }}` | `9` |
| `$to_int(value, default)` | Convert to an integer | `&:{{ $to_int('42') }}` | `42` |
| `$to_float(value, default)` | Convert to a float | `&:{{ $to_float('4.2') }}` | `4.2` |
| `$round(x)` | Round to the nearest integer | `&:{{ $round(2.6) }}` | `3` |
| `$floor(x)` | Round down | `&:{{ $floor(2.6) }}` | `2` |
| `$ceil(x)` | Round up | `&:{{ $ceil(2.1) }}` | `3` |
| `$abs(x)` | Absolute value | `&:{{ $abs(-3) }}` | `3.0` |
| `$random(min, max)` | Random integer. Defaults `0`, `100` | `&:{{ $random(1, 6) }}` | `4` |
| `$to_currency(amount, currency)` | Currency text. Default `USD` | `{{ $to_currency(1234.5) }}` | `USD 1,234.50` |
| `$to_percent(value, decimals)` | Percentage text. Default `1` decimal | `{{ $to_percent(0.256) }}` | `25.6%` |
| `$to_ordinal(value)` | Ordinal text | `{{ $to_ordinal(3) }}` | `3rd` |

### Arrays and Objects

| Helper | Description | Example | Result |
|---|---|---|---|
| `$len(value)` | Length of a string, array, or object. Alias `$length` | `&:{{ $len(tags) }}` | `3` |
| `$sum(array)` | Sum of numbers | `&:{{ $sum(scores) }}` | `6.0` |
| `$avg(array)` | Average of numbers | `&:{{ $avg(scores) }}` | `2.0` |
| `$min(array)` | Smallest number | `&:{{ $min(scores) }}` | `1.0` |
| `$max(array)` | Largest number | `&:{{ $max(scores) }}` | `3.0` |
| `$sortby(array, key, reverse)` | Sort objects by a key | `&:{{ $sortby(items, 'qty', true) }}` | Highest `qty` first |
| `$groupby(array, key)` | Group objects by a key | `&:{{ $groupby(items, 'type') }}` | `{"a": [...], "b": [...]}` |

### Conditions and Validation

| Helper | Description | Example | Result |
|---|---|---|---|
| `$cond(condition, yes, no)` | If/else. Alias `$condition` | `{{ $cond(total > 100, 'large', 'small') }}` | `large` |
| `$default(value, fallback)` | `fallback` when `value` is empty. `0` is kept | `{{ $default(nickname, 'Guest') }}` | `Guest` |
| `$is_empty(value)` | Value is empty | `&:{{ $is_empty(tags) }}` | `false` |
| `$is_null(value)` | Value is null or empty | `&:{{ $is_null(deleted_at) }}` | `true` |
| `$is_truthy(value)` | Value is truthy | `&:{{ $is_truthy(flag) }}` | `true` |
| `$is_falsey(value)` | Value is falsy | `&:{{ $is_falsey(flag) }}` | `false` |
| `$is_not(value)` | Logical NOT | `&:{{ $is_not(archived) }}` | `true` |
| `$is_email(value)` | Valid email address | `&:{{ $is_email(email) }}` | `true` |
| `$is_url(value)` | Valid URL | `&:{{ $is_url(website) }}` | `true` |
| `$is_ip(value)` | Valid IP address | `&:{{ $is_ip(ip) }}` | `true` |

### Encoding

| Helper | Description | Example | Result |
|---|---|---|---|
| `$b64_encode(text)` | Base64-encode text | `{{ $b64_encode('hi') }}` | `aGk=` |
| `$url_encode(text)` | URL-encode text | `{{ $url_encode('a b&c') }}` | `a%20b%26c` |

---

## Filters

Apply filters with `|`. Chain them left to right. Filters that take arguments use parentheses.

```json
{
  "title": "{{ raw_title | trim | title }}",
  "publish_on": "{{ $now() | date_shift('+1 day') | date_format('YYYY-MM-DD') }}"
}
```

| Filter | Description | Example |
|---|---|---|
| `trim` | Remove surrounding whitespace | `{{ name \| trim }}` |
| `upper`, `uppercase` | Uppercase | `{{ code \| upper }}` |
| `lower`, `lowercase` | Lowercase | `{{ email \| lower }}` |
| `title`, `titlecase` | Capitalize each word | `{{ name \| title }}` |
| `capitalize` | Capitalize the first letter | `{{ name \| capitalize }}` |
| `slugify` | URL-friendly slug | `{{ title \| slugify }}` |
| `strip_html` | Remove HTML tags | `{{ body \| strip_html }}` |
| `replace(old, new)` | Replace text | `{{ sku \| replace('-', '') }}` |
| `truncate(length)` | Shorten to a character length | `{{ title \| truncate(40) }}` |
| `len`, `length` | Length | `&:{{ tags \| len }}` |
| `join(separator)` | Join an array | `{{ tags \| join(', ') }}` |
| `first`, `last` | First or last item | `{{ tags \| first }}` |
| `int`, `float`, `string`, `list` | Convert type | `&:{{ qty \| int }}` |
| `round`, `abs`, `floor`, `ceil` | Number rounding and sign | `&:{{ price \| round }}` |
| `incr(by)`, `decr(by)` | Add or subtract | `&:{{ score \| incr(5) }}` |
| `date_format(format)` | Format a date | `{{ created \| date_format('MMM D, YYYY') }}` |
| `date_shift(shift)` | Shift a date | `{{ $now() \| date_shift('2days') }}` |
| `to_currency(currency)` | Currency text | `{{ price \| to_currency('EUR') }}` |
| `to_percent(decimals)` | Percentage text | `{{ rate \| to_percent(2) }}` |
| `to_int`, `to_float` | Convert to a number | `&:{{ qty \| to_int }}` |
| `sha256` | SHA-256 hex digest | `{{ email \| sha256 }}` |
| `b64_encode`, `url_encode` | Encode text | `{{ query \| url_encode }}` |
| `default(value)` | Fallback for a missing field | `{{ nickname \| default('Guest') }}` |

---

## Common Patterns

### Display Name

```json
{
  "display_name": "{{ $titlecase($trim(first_name ~ ' ' ~ last_name)) }}"
}
```

### Order Totals

```json
{
  "subtotal": "&:{{ price * quantity }}",
  "tax": "&:{{ $round(price * quantity * 0.08) }}",
  "is_free_shipping": "&:{{ price * quantity >= 50 }}"
}
```

### Expiration Date

```json
{
  "expires_at": "{{ $now() | date_shift('30days') }}"
}
```

### Content Preview

```json
{
  "preview": "{{ $truncate($strip_html(body), 25, '...') }}",
  "word_count": "&:{{ $count_words($strip_html(body)) }}"
}
```

### Masking Sensitive Values

```json
{
  "card_display": "{{ $maskstr(card_last_digits, 4) }}",
  "email_hash": "{{ $sha256($lowercase(email)) }}"
}
```

---

## Best Practices

- **Use `&:` for numbers and booleans.** `{{ }}` always returns a string, so `"{{ price * 2 }}"` saves `"24"`, not `24`.
- **Keep expressions short.** Move complex logic to your application code.
- **Don't build counters with templates.** Template and operator writes read the document first, so concurrent writes can overlap.

:::

---

## Next Steps

- [Data](/data.md) - Insert, update, and upsert documents
- [Data Operators](/data.md#data-operators) - `$incr`, `$push`, `$unset`, and more
- [Query Syntax](/queries.md) - Filter documents
