Semaphor

Template Expressions

Write dynamic SQL with param and filter expressions, conditional blocks, and list expansion

Template expressions let you write dynamic SQL that reads dashboard controls (via {{ param(...) }}), reads active filters (via {{ filter(...) }}), branches with {% if %} blocks, and expands multi-select values into IN lists. The syntax is a small subset of Jinja, so it will feel familiar if you have used dbt or similar tools.

For simple cases, keep using the dedicated forms: [[param.x]] to substitute a single control value, or {{ filters | where }} to subscribe a card to the dashboard filter bar. Use template expressions when you need conditional SQL, multi-value IN lists, explicit defaults, or logic that combines parameters and filters.


When to use template expressions

NeedSyntax
Insert a single value[[param.region]] or {{ param('region') }}
Expand a multi-select into IN (...){{ param('ids').list }}
Branch on a control value{% if param('grain').value == 'day' %}...{% endif %}
Fall back to a default{{ param('grain', 'month') }}
Gate a predicate on a filter being set{% if filter('segment').present %}...{% endif %}

Accessors at a glance

param and filter expose the same set of accessors. Output accessors render SQL text inside {{ ... }}; condition accessors return raw values for use inside {% if ... %} blocks.

AccessorUse inReturnsExample
.sql{{ ... }} outputSQL-quoted scalar{{ param('region').sql }} renders 'West'
.list{{ ... }} outputParenthesized SQL list for IN (...)WHERE id IN {{ param('ids').list }} renders IN (1, 2, 3)
.list_sql{{ ... }} outputAlias for .list--
.value{% if ... %} conditionRaw scalar for comparisons{% if param('grain').value == 'day' %}
.values{% if ... %} conditionRaw list for comparisons{% if 'VIP' in filter('tier').values %}
.present{% if ... %} conditionBoolean; true if a non-null value exists{% if filter('region').present %}

Bare form is shorthand for `.sql`

{{ param('id') }} is exactly equivalent to {{ param('id').sql }} -- the bare form renders the SQL-quoted literal. The same holds for {{ filter('name') }} and {{ filter('name').sql }}. Use the explicit .sql accessor only when it makes intent clearer; most of the time, the bare form is all you need.

Accessors do not mix contexts

.sql, .list, and .list_sql are only valid in output positions ({{ ... }}). .value, .values, and .present are only valid inside {% if %} conditions. Using the wrong one raises a template error.


Parameter expressions

{{ param('id') }} resolves to the dashboard or card control with the matching ID. Card-scoped controls take precedence over dashboard-scoped controls with the same ID, matching the behavior of [[param.id]].

Single value

Card SQL
SELECT * FROM orders WHERE tenant = {{ param('tenant_id') }}

With tenant_id set to acme_corp:

Resolved SQL
SELECT * FROM orders WHERE tenant = 'acme_corp'

Numeric and boolean controls render without quotes:

Card SQL
SELECT {{ param('account_id') }} AS account_id

With account_id set to 42:

Resolved SQL
SELECT 42 AS account_id

List expansion

Multi-select controls render as parenthesized SQL lists ready for IN:

Card SQL
SELECT category, COUNT(*) AS order_count
FROM orders
WHERE id IN {{ param('ids').list }}
GROUP BY category

With ids set to [1, 2, 3]:

Resolved SQL
SELECT category, COUNT(*) AS order_count
FROM orders
WHERE id IN (1, 2, 3)
GROUP BY category

The simpler WHERE id IN ([[param.ids]]) form works for the same case. Use .list when you want the parentheses emitted automatically.

Defaults

Pass a second argument to param to supply a fallback when the control has no value:

Card SQL
SELECT {{ param('grain', 'month') }} AS grain

If the viewer has not set grain:

Resolved SQL
SELECT 'month' AS grain

Guarding on .present

To branch on whether a control has a value, use .present:

Card SQL
SELECT * FROM orders
{% if param('region').present %}
  WHERE region = {{ param('region') }}
{% endif %}

Use not param('x').present to run logic specifically when a control is empty -- for example, to apply a default row limit:

Card SQL
SELECT * FROM orders
ORDER BY order_date DESC
{% if not param('row_limit').present %}
  LIMIT 100
{% endif %}

A few behaviors worth knowing:

  • Optional controls with no value resolve to NULL under the [[param.x]] syntax, but report .present == false under template expressions. That is the cleanest way to skip a predicate entirely.
  • If a control is required and the viewer has not made a selection, the card shows the same error state as today. The query does not run, regardless of which syntax you use.

Filter expressions

{{ filter('name') }} complements the {{ filters | where }} helper. Use it when you need a specific filter's value, a list of its selected values, or conditional logic that depends on whether the filter is active.

Single value

Card SQL
SELECT * FROM orders WHERE region = {{ filter('region') }}
Resolved SQL
SELECT * FROM orders WHERE region = 'West'

List expansion

Card SQL
SELECT * FROM orders WHERE status IN {{ filter('status').list }}
Resolved SQL
SELECT * FROM orders WHERE status IN ('open', 'closed')

Guarding on .present

Gate a predicate on the filter being set:

Card SQL
SELECT * FROM orders
WHERE 1 = 1
  {% if filter('segment').present %}
    AND segment = {{ filter('segment') }}
  {% endif %}

When segment is not active, the {% if %} block is dropped entirely.

Use not filter('x').present to run logic specifically when a filter is missing -- for example, to return zero rows until the viewer picks a tenant:

Card SQL
SELECT * FROM orders
WHERE 1 = 1
  {% if not filter('tenant_id').present %}
    AND 1 = 0
  {% else %}
    AND tenant_id = {{ filter('tenant_id') }}
  {% endif %}

With no tenant_id filter applied:

Resolved SQL
SELECT * FROM orders
WHERE 1 = 1
    AND 1 = 0

Conditional blocks

{% if %}, {% elif %}, {% else %}, and {% endif %} let you include or exclude SQL fragments based on control and filter state. Conditions can combine param and filter expressions freely.

Grain switcher driven by a control:

Card SQL
SELECT
  {% if param('grain').value == 'day' %}
    DATE_TRUNC('day', order_date)
  {% elif param('grain').value == 'week' %}
    DATE_TRUNC('week', order_date)
  {% else %}
    DATE_TRUNC('month', order_date)
  {% endif %} AS bucket,
  COUNT(*) AS orders
FROM orders
GROUP BY 1

With grain = 'day':

Resolved SQL
SELECT
  DATE_TRUNC('day', order_date) AS bucket,
  COUNT(*) AS orders
FROM orders
GROUP BY 1

Supported operators inside {% if %}:

  • Comparison: ==, !=, <, >, <=, >=
  • Boolean: and, or, not
  • Membership: in (e.g. {% if 'VIP' in filter('tier').values %})

Combining parameters and filters

A card that combines a required tenant parameter, an optional segment filter, and a grain switcher:

Card SQL
SELECT
  {% if param('grain').value == 'day' %}
    DATE_TRUNC('day', order_date)
  {% else %}
    DATE_TRUNC('month', order_date)
  {% endif %} AS bucket,
  COUNT(*) AS orders
FROM orders
WHERE tenant_id = {{ param('tenant_id') }}
  {% if filter('segment').present %}
    AND segment IN {{ filter('segment').list }}
  {% endif %}
GROUP BY 1
ORDER BY 1

With tenant_id = 'acme_corp', grain = 'day', and a segment filter of ['enterprise', 'mid_market']:

Resolved SQL
SELECT
  DATE_TRUNC('day', order_date) AS bucket,
  COUNT(*) AS orders
FROM orders
WHERE tenant_id = 'acme_corp'
    AND segment IN ('enterprise', 'mid_market')
GROUP BY 1
ORDER BY 1

Safety

Template expressions SQL-quote and escape values automatically. You never wrap them in quotes yourself.

Card SQL
SELECT * FROM orders WHERE name = {{ param('name') }}

With name set to O'Brien:

Resolved SQL
SELECT * FROM orders WHERE name = 'O''Brien'

Do not wrap expressions in quotes

Writing '{{ param('region') }}' is an error -- the same rule as the [[param.region]] placeholder syntax. Multi-value controls render as SQL lists, not concatenated strings, so they cannot be string-interpolated either.


What's supported

Supported:

  • param(id) and param(id, default) expressions
  • filter(name) expressions
  • Accessors: .sql, .list, .list_sql, .value, .values, .present
  • {% if %}, {% elif %}, {% else %}, {% endif %} blocks
  • Comparison operators: ==, !=, <, >, <=, >=
  • Boolean operators: and, or, not
  • Membership: in

Not supported:

  • Loops ({% for %})
  • Variable assignment ({% set %})
  • Raw blocks ({% raw %})
  • Macros and custom filter pipelines
  • Any template syntax on cached-source connections (S3, S3 Tables, Google Sheets, uploaded files) -- those datasets reject template expressions at save and run time

On this page