These are declarations — placeholders the SQL author defines. Schema lives in metabase.lib.schema.template-tag. Each tag has a :type:
:type |
Purpose |
|---|---|
:text, :number, :date, :boolean |
Scalar variable substitution: WHERE x = {{var}} |
:dimension |
Field filter — has :dimension [:field ...] and :widget-type; expands to a full WHERE clause |
:snippet |
{{snippet: foo}} — resolves to a NativeQuerySnippet row (verbatim SQL) |
:card |
{{#123}} — resolves to another card's compiled SQL as a subquery |
:table |
References a Table by id, optionally with :source-filters and :emit-alias |
:temporal-unit |
Injects a temporal bucketing unit into a field ref |
These are values passed at query time from the API. Schema in metabase.lib.schema.parameter. A ::parameter is {:id :type [:value] [:target] ...}.
The :target routes the value to the right tag or field — four shapes:
[:dimension [:template-tag "my_tag"]] ; Field filter tag (by name)
[:dimension [:template-tag {:id <param-id>}]] ; Field filter tag (by id, preferred since v44)
[:variable [:template-tag "my_tag"]] ; Raw-value tag
[:dimension [:field 100 nil]] ; MBQL field directlymetabase.lib.parameters provides helpers like parameter-target-template-tag-name, parameter-target-field-id, etc. to pick these apart.
Defined in metabase.query-processor.preprocess:
resolve-referenced-card-resources ← pre-fetches cards/snippets transitively
→ parameters/substitute-parameters ← THE parameter middleware
resolve-source-tables
auto-bucket-datetimes
... (joins, remaps, implicit clauses, sandboxing, etc.)
resolve-referenced-card-resources (middleware/resolve_referenced.clj) runs first: it walks template tags, finds every :card and :snippet tag transitively, builds a dependency graph (via weavejester.dependency), throws on circular references, and warms the metadata provider. It runs before parameter substitution because substitution needs those cards/snippets loaded.
Four phases:
metabase.lib.parse/parse tokenizes the SQL string, recognizing [[, ]], {{, }}, and SQL comment boundaries. metabase.lib.parameters.parse/parse wraps this and emits typed records:
::param—{:lib/type ::param :k "tag-name"}::optional—{:lib/type ::optional :args [...]}— the[[…]]block::function-param— for special function invocations
Tag names are normalized: {{snippet: foo}} and {{#123}} get recognized and stripped.
metabase.driver.common.parameters.values/query->params-map iterates :template-tags and dispatches per :type:
:dimension→FieldFilterrecord with resolved column metadata + matched parameter values:card→ compiles the card viaqp.compile/compile, wraps inReferencedCardQuery(with its JDBC args):snippet→ loads from metadata provider →ReferencedQuerySnippet {:snippet-id :content}:temporal-unit→TemporalUnitrecord with the field and new unit- Raw value types → bare value (with default/required handling)
metabase.driver.sql.parameters.substitute/substitute reduces the parsed fragments:
- String fragments → verbatim
Paramtokens → look up in param map, dispatch by record typeOptionalblocks ([[…]]) → if any key ismissing(no value), the entire block is discarded; otherwise it expands normally- A
FieldFilterwith no value substitutes"1 = 1"outside optionals, signalsmissinginside them ReferencedQuerySnippettriggers recursive re-parsing of the snippet content (snippets can contain{{tags}})
metabase.driver.sql.parameters.substitution/->replacement-snippet-info (multimethod on [driver class]):
| Value type | SQL output |
|---|---|
| Scalar | ? + JDBC arg |
DateRange/DateTimeRange |
BETWEEN ? AND ? or >= / < |
IPersistentVector |
?, ?, ? (IN list) |
FieldFilter |
Builds MBQL filter via params.ops/to-clause, renders via HoneySQL |
ReferencedCardQuery |
(SELECT ...) via make-nestable-sql + card's JDBC args |
ReferencedQuerySnippet |
:content inserted verbatim (no JDBC args) |
ReferencedTableQuery |
"schema"."table" with optional WHERE and alias |
In metabase.query-processor.middleware.parameters:
(defn substitute-parameters [query]
(-> query
hoist-database-for-snippet-tags ; stamps :database id into every :snippet tag
expand-parameters))expand-parameters:
-
move-top-level-params-to-stage— distributes top-level:parametersto the stage they target (using:stage-numberfrom the target options, offset to account for source-card stages prepended during preprocessing). Renames top-level:parametersto:user-parameters. Sets:qp/skip-result-metadata-persistencetrue if non-default values were applied. -
expand-all— walks every stage vialib.walk/walk-stages, dispatching to:- MBQL stage →
qp.mbql/expand - Native stage →
qp.native/expand-stage
Both strip
:parametersand:template-tagsfrom the stage after expansion. - MBQL stage →
metabase.query-processor.middleware.parameters.mbql/expand reduces over the stage's :parameters:
:temporal-unitparams →update-breakout-unitswaps the unit on the matching breakout- Operator params (
:number/between,:string/contains, etc.) →params.ops/to-clause→lib.filter/add-filter-to-stage - Date-typed params →
params.dates/date-string->filter(parses relative ranges, exclusions, etc.) → filter - Single values →
lib/= field-ref parsed-value - Multiple values → recursively built clauses glued with
lib/or
| Key | Location | Purpose | Lifecycle |
|---|---|---|---|
:template-tags |
Native stage | Declarations — name, type, default, widget-type, dimension | Created when SQL is parsed; stripped after substitute-parameters |
:parameters (top-level) |
Query root | Values from API (dashboard filters, etc.) | Distributed to stages by move-top-level-params-to-stage, then consumed |
:parameters (stage-level) |
Any stage | Post-distribution values | Stripped after expand-all |
| Aspect | {{my_var}} |
{{snippet: foo}} |
|---|---|---|
| Value source | API :parameters matched by tag name |
NativeQuerySnippet row in app DB |
| SQL output | ? placeholder + JDBC args (or field op ? for field filters) |
:content interpolated verbatim |
| Recursive expansion | No | Yes — snippet content is re-parsed, so it can contain {{tags}} |
[[…]] interaction |
Empty value drops the block | Almost always present |
| DB id requirement | Not needed | hoist-database-for-snippet-tags injects :database id before expansion |
Cards ({{#123}}) are in between — they expand to (SELECT ...) via make-nestable-sql, but do contribute JDBC args (the inner card may itself be parameterized).
These are two separate systems:
| Layer | Namespaces | Role |
|---|---|---|
| Application-side | metabase.parameters.* |
Powers parameter widget UI — chain filtering, field value lookup, stored declarations |
| QP middleware | metabase.query-processor.middleware.parameters.*, metabase.driver.sql.parameters.* |
Consumes the widget output (the :parameters list) and rewrites the query |
Key metabase.parameters.* namespaces:
parameters.schema— stored Card/Dashboard parameter declarations and Toucan transformsparameters.params— field ID resolution for parameter targets; Toucan hydration of:param_fieldsparameters.chain-filter— builds MBQL queries to populate parameter dropdownsparameters.field/field-values— values for parameter widgets (search, list, FK remapping)parameters.custom-values— static-list or card-as-source-of-valuesparameters.dashboard— bridges chain filtering to dashboard API endpoints
The bridge between them is the schema layer: metabase.lib.schema.parameter defines the wire format; metabase.parameters.schema defines the stored format. API endpoints normalize stored declarations + user input into the :parameters list before invoking the QP.
| File | Purpose |
|---|---|
src/metabase/lib/schema/template_tag.cljc |
Template tag Malli schemas |
src/metabase/lib/schema/parameter.cljc |
Parameter value schemas, types registry, parameter-type-and-widget-type-allowed-together? |
src/metabase/lib/parameters.cljc |
Target-parsing helpers |
src/metabase/lib/parameters/parse.cljc |
SQL tokenizer/parser |
src/metabase/query_processor/middleware/parameters.clj |
Top-level substitute-parameters middleware |
src/metabase/query_processor/middleware/parameters/mbql.clj |
MBQL stage expansion |
src/metabase/query_processor/middleware/parameters/native.clj |
Native stage expansion |
src/metabase/query_processor/middleware/resolve_referenced.clj |
Pre-fetch cards/snippets |
src/metabase/driver/common/parameters/values.clj |
query->params-map (legacy, still active) |
src/metabase/driver/sql/parameters/substitute.clj |
Token-sequence substitution |
src/metabase/driver/sql/parameters/substitution.clj |
->replacement-snippet-info multimethod |
src/metabase/parameters/chain_filter.clj |
Parameter dropdown query building |
Note:
metabase.driver.common.parameters.*namespaces are deprecated and flagged for migration tometabase.lib.parameters.*andmetabase.query-processor.parameters.*as part of the pMBQL transition — but the SQL driver still uses the legacy form becauseexpand-stageconverts pMBQL stages to legacy-MBQL vialib/->legacy-MBQLbefore callingdriver/substitute-native-parameters.