Advanced Filters
Overviewâ
Advanced Filters let users build filter queries from conditions joined with AND / OR, nest them in condition groups, and pick an operator per condition (is_not, greater_than, is_empty, âŠ). The standard filter bar only supports "equals / between".
Advanced Filters are available automatically on every index page that defines :single_select, :multi_select, :range or :date filters. No extra DSL or generator is needed.
Usageâ
Declare filters as usual:
cm_admin do
cm_index do
filter %i[name email], :search, placeholder: 'Search'
filter :status, :single_select, helper_method: :status_options
filter :age, :range, placeholder: 'Age'
filter :created_at, :date, placeholder: 'Created at'
end
end
On the index page, open Filters â Add advanced filter, then:
- Pick a field, an operator, and a value.
- Use Add condition to add more rows, and switch the connector between And and Or.
- Use Add condition group to nest conditions, for example
status is active AND (age > 60 OR created_at between âŠ). - Click Apply.
While Advanced Filters are active, the standard filter bar is hidden. Standard filters, including their default_values, are ignored: only search and the advanced conditions are applied. Exports follow the same rule.
Operatorsâ
| Filter type | Operators | Default |
|---|---|---|
:single_select, :multi_select | is, is_not, is_empty, is_not_empty | is |
:date | between, not_between, is_empty, is_not_empty | between |
:range | between, not_between, is, is_not, greater_than, less_than, greater_than_or_equal_to, less_than_or_equal_to, is_empty, is_not_empty | between |
between/not_betweentake a range value ("18 to 30","2026-01-01 to 2026-01-31").is_empty/is_not_emptytake no value and check forNULL.- All other operators take a single value.
Conditions are evaluated left to right: A OR B AND C means (A OR B) AND C. Use a condition group when you need a different precedence.
Custom Filter Scopes (filter_with)â
Filters that use filter_with work in Advanced Filters too. The scope receives the selected operator as a second argument, condition, which is nil when the scope is called from the standard filter bar:
# :single_select / :multi_select
scope :by_company_country, lambda { |value, condition = nil|
left_joins(:company).ct_dropdown_filter_condition(column: :country, value:, condition:, table: 'companies')
}
# :range / :date. `value` is the raw string, e.g. "18 to 30" or "18"
scope :by_company_size, lambda { |value, condition = nil|
left_joins(:company).ct_range_filter_condition(column: :employee_count, value:, condition:, table: 'companies')
}
ct_dropdown_filter_condition and ct_range_filter_condition are available on every ActiveRecord model and implement all of the operators above. table: defaults to the model's own table. If you write the logic yourself, remember to handle is_empty / is_not_empty, which are called with a blank value. See Custom Filter Method.
URL Formatâ
Filters, both standard and advanced, are stored in the filters query param as zlib-compressed, base64url-encoded JSON. Once decoded, an advanced filter looks like this:
{
"search": "john",
"advanced": [
{
"filter_type": "single_select",
"filter_name": "status",
"condition": "is",
"value": "active",
"operator": "and"
},
{
"filter_type": "group",
"operator": "and",
"value": [
{
"filter_type": "range",
"filter_name": "age",
"condition": "greater_than",
"value": "60",
"operator": "and"
},
{
"filter_type": "date",
"filter_name": "created_at",
"condition": "between",
"value": "2026-01-01 to 2026-01-31",
"operator": "or"
}
]
}
]
}
operator joins an item to the items before it. The first item's operator is ignored.
Use the helpers below whenever you read or build this param in Ruby. The legacy nested filters[...] hash is still accepted:
filters = CmAdmin::Utils.decode_filter_params(params[:filters]) || {}
cm_admin.cm_index_user_path(filters: CmAdmin::Utils.encode_filter_params(filters))
To save a set of advanced filters and reuse it later, see Saved Views.