Docs / Build Workflow

Filter — select

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 when param is not set.
  • label — display label above the dropdown.
  • param — the Malloy parameter name to bind the selected value to. Defaults to id.
  • default — initial selected value. Must match the id of one of the options.
  • options — array of {id, label} objects. Use this for a static option list.
  • options_query — string in the format path/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 (or indicator_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 id of one of the options. For dynamic lists, ensure the "all" sentinel is included in the query result (or in options alongside it).
  • The filter takes too long to load. The options_query runs 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 label human-readable and the id stable; the id is 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.