When to use select
Use select when the user needs to pick one value from a known set: a status, a region, a brand, a category. The dropdown reflects either a static option list declared in YAML or a dynamic list resolved by running a Malloy query at page load.
Reach for one of the date filters instead when the input is a date or a date range. Reach for cross-filtering when the user is meant to drill in by clicking a chart rather than picking from a dropdown.
Required fields
type: select
Either options or options_query must be present (and one of them only).
Optional fields
id— internal identifier; also the default parameter name whenparamis not set.label— display label above the dropdown.param— the Malloy parameter name to bind the selected value to. Defaults toid.default— initial selected value. Must match theidof one of the options.options— array of{id, label}objects. Use this for a static option list.options_query— string in the formatpath/to/source.mal::QueryName. Looky runs this query at page load and turns the rows into options.
Static options
Declare the list inline. Best for small, stable enumerations — statuses, on/off toggles, periodicities.
filters:
- type: select
id: status
label: Status
param: status
default: all
options:
- { id: all, label: All }
- { id: active, label: Active }
- { id: cancelled, label: Cancelled }
- { id: refunded, label: Refunded }
Each option must have an id (the value sent to the query) and a label (the text shown in the dropdown). The id is what your Malloy model receives — design the model parameter to accept the same shape.
Dynamic options (options_query)
Use when the option list comes from data — brands, customers, countries, anything that changes over time.
filters:
- type: select
id: brand
label: Brand
param: brand
options_query: "models/ec_revenue.malloy::brand_options"
default: all
The query brand_options must return rows with at least id and label columns. The Malloy side typically looks like:
view: brand_options is {
group_by:
id is brand
label is brand
order_by: brand asc
}
Each row of the query becomes one option. Looky normalises every row to an {id, label}:
- id — first non-null of
id,indicator_code,group_code. - label — first non-null of
label,indicator_label,group_label. - sort key — if a row has
sort_order(orindicator_order/group_order), options are ordered numerically by that field; otherwise they are ordered alphabetically by label.
Rows missing both an id and a label are dropped.
Default value
If default is set, it is the value of the parameter when the page loads. The reset button restores this value. If no default is set, the parameter is unset until the user picks a value, and the underlying query must accept the parameter's absence (typically by declaring its own default).
For dynamic option lists, declaring an "all" sentinel option and using it as the default is a common pattern — your Malloy model treats "all" as "no filter".
How the value reaches the Malloy query
The chosen option's id is sent as the named parameter — by default named after the filter's id, or param if you set it. The Malloy model declares a parameter with the matching name (after the p_ prefix is stripped — see Malloy support) and uses it in the query.
# in the Malloy model
##! experimental.parameters
source: orders(
p_status::string is "all"
) is bigquery.table('...') extend {
view: detail is {
where:
p_status = "all" or status = p_status
select:
*
}
}
Adapter differences
select values are typically strings or short identifiers; all three adapters bind them identically. The only edge case is when the parameter on the model side is declared as a date or timestamp — that pattern follows the rules at Source adapter differences.
Worked examples
Static enumeration with an "all" sentinel:
filters:
- type: select
id: status
label: Status
param: status
default: all
options:
- { id: all, label: All }
- { id: active, label: Active }
- { id: cancelled, label: Cancelled }
Dynamic from a query, with custom parameter binding:
filters:
- type: select
id: brand_filter
label: Brand
param: p_brand
options_query: "models/ec_revenue.malloy::brand_options"
default: all
Multiple selects on the same dashboard, each filtering a different dimension:
filters:
- type: select
id: country
label: Country
options_query: "models/ec_revenue.malloy::country_options"
default: all
- type: select
id: channel
label: Channel
options:
- { id: all, label: All channels }
- { id: organic, label: Organic }
- { id: paid, label: Paid }
- { id: direct, label: Direct }
default: all
Common pitfalls
- The default value does not load. The default must match the
idof one of the options. For dynamic lists, ensure the "all" sentinel is included in the query result (or inoptionsalongside it). - The filter takes too long to load. The
options_queryruns at page load. If the underlying query is slow, the dropdown will block. Pre-aggregate the option list in the Malloy query, or cache it via a sidecar. - The Malloy model does not accept the parameter. Declare the parameter in the model with a sensible default, so the query still runs when the parameter is missing or set to a sentinel.
- The option labels and the option ids drift apart. Keep the
labelhuman-readable and theidstable; theidis the contract with the query, not the label. - Capitalisation mismatch between the option id and the data. The model's where-clause is exact-match by default. Either normalise both sides in the model (
lower(status) = lower(p_status)) or align the option ids with the data exactly.