Dimensions reference
The columns of your tables, and every option for defining and configuring them in YAML
Dimensions usually match 1:1 with columns in your dbt models (see additional dimensions for counterexamples).
Adding dimensions to your project
For a dimension to appear in Qyra, you just need to declare it in your dbt model's YAML file.
models:
- name: my_model
columns:
- name: user_id # will be "User id" in Qyra
description: "Unique identifier for a user."Write these entries by hand, or generate one for every column in a model with qyra generate.
Dimensions cannot use window functions in their sql. A dimension's SQL is inserted into the same SELECT as the main GROUP BY in the generated query, and SQL does not allow window functions in that position. If you need window-function logic, either model it directly in your dbt model (so the value is precomputed before Qyra queries it), or add a table calculation on the query results. Qyra also supports a small set of built-in window operations through post calculation metrics (percent_of_previous, percent_of_total, running_total).
Dimension configuration
To customize the dimension, you can do it in your dbt model's YAML file under the meta tag. The syntax depends on your dbt version.
If you want to declare multiple dimensions based on the same column, check additional dimensions section.
models:
- name: sales_stats
config:
meta:
group_details:
finance:
label: Finance
description: Finance-related fields.
joins:
- join: web_sessions
sql_on: ${web_sessions.date} = ${sales_stats.date}
columns:
- name: revenue_gbp_total_est
description: 'Total estimated revenue in GBP based on forecasting done by the finance team.'
config:
meta:
dimension:
type: number
label: 'Total revenue' # this is the label you'll see in Qyra
description: 'My custom description' # you can override the description you'll see in Qyra here
sql: 'IF(${TABLE}.revenue_gbp_total_est = NULL, 0, ${registered_user_email})' # custom SQL applied to the column from dbt used to define the dimension
hidden: false
format: '[$£]#,##0.00' # GBP rounded to two decimal points
groups: ['finance']
- name: forecast_date
description: 'Date of the forecasting.'
config:
meta:
dimension:
type: date
time_intervals: ['DAY', 'WEEK', 'MONTH', 'QUARTER'] # not required: the default time intervals for dates are `['DAY', 'WEEK', 'MONTH', 'YEAR']`
urls:
- label: 'Open in forecasting tool'
url: 'https://finance.com/forceasts/weeks/${ value.raw }'
- label: Open in Google Calendar
url: 'https://calendar.google.com/calendar/u/0/r/day/${ value.formatted |split: "-" |join: "/"}'
required_attributes:
is_admin: 'true'models:
- name: sales_stats
meta:
group_details:
finance:
label: Finance
description: Finance-related fields.
joins:
- join: web_sessions
sql_on: ${web_sessions.date} = ${sales_stats.date}
columns:
- name: revenue_gbp_total_est
description: 'Total estimated revenue in GBP based on forecasting done by the finance team.'
meta:
dimension:
type: number
label: 'Total revenue' # this is the label you'll see in Qyra
description: 'My custom description' # you can override the description you'll see in Qyra here
sql: 'IF(${TABLE}.revenue_gbp_total_est = NULL, 0, ${registered_user_email})' # custom SQL applied to the column from dbt used to define the dimension
hidden: false
format: '[$£]#,##0.00' # GBP rounded to two decimal points
groups: ['finance']
- name: forecast_date
description: 'Date of the forecasting.'
meta:
dimension:
type: date
time_intervals: ['DAY', 'WEEK', 'MONTH', 'QUARTER'] # not required: the default time intervals for dates are `['DAY', 'WEEK', 'MONTH', 'YEAR']`
urls:
- label: 'Open in forecasting tool'
url: 'https://finance.com/forceasts/weeks/${ value.raw }'
- label: Open in Google Calendar
url: 'https://calendar.google.com/calendar/u/0/r/day/${ value.formatted |split: "-" |join: "/"}'
required_attributes:
is_admin: 'true'type: model
name: sales_stats
group_details:
finance:
label: Finance
description: Finance-related fields.
joins:
- join: web_sessions
sql_on: ${web_sessions.date} = ${sales_stats.date}
dimensions:
- name: revenue_gbp_total_est
description: 'Total estimated revenue in GBP based on forecasting done by the finance team.'
type: number
label: 'Total revenue' # this is the label you'll see in Qyra
sql: 'IF(${TABLE}.revenue_gbp_total_est = NULL, 0, ${registered_user_email})' # custom SQL applied to the column from dbt used to define the dimension
hidden: false
format: '[$£]#,##0.00' # GBP rounded to two decimal points
groups: ['finance']
- name: forecast_date
description: 'Date of the forecasting.'
type: date
time_intervals: ['DAY', 'WEEK', 'MONTH', 'QUARTER'] # not required: the default time intervals for dates are `['DAY', 'WEEK', 'MONTH', 'YEAR']`
urls:
- label: 'Open in forecasting tool'
url: 'https://finance.com/forceasts/weeks/${ value.raw }'
- label: Open in Google Calendar
url: 'https://calendar.google.com/calendar/u/0/r/day/${ value.formatted |split: "-" |join: "/"}'
required_attributes:
is_admin: 'true'The table below shows all the dimension properties you can customize:
| Property | Required | Value | Description |
|---|---|---|---|
| label | No | string | Custom label. If you set this property, this is what you'll see in Qyra instead of the dimension name. |
| type | No | Dimension type | The dimension type is automatically pulled from your table schemas in Qyra but you can override the type using this property. |
| description | No | string | Description of the dimension in Qyra. You can use this to override the description you have for the dimension in dbt. |
| sql | No | string | Custom SQL applied to the column used to define the dimension. |
| time_intervals | No | 'default' or OFF or an array[] containing elements of date, numeric, string options, or custom granularity names | 'default' (or not setting the time_intervals property) will be converted into ['DAY', 'WEEK', 'MONTH', 'QUARTER', 'YEAR'] for dates and ['RAW', 'DAY', 'WEEK', 'MONTH', 'QUARTER', 'YEAR'] for timestamps; if you want no time intervals set 'OFF'. You can also include custom granularity names defined in qyra.config.yml. |
| hidden | No | boolean | If set to true, the dimension is hidden from Qyra. By default, this is set to false if you don't include this property. Hidden dimensions are also excluded from drilldowns (View underlying data). |
| compact | No | string | This option will compact the number value (e.g. 1,500 to 1.50K). Currently supports one of the following: ['auto', 'thousands', 'millions', 'billions', 'trillions', 'kilobytes', 'megabytes', 'gigabytes', 'terabytes', 'petabytes', 'kibibytes', 'mebibytes', 'gibibytes', 'tebibytes', 'pebibytes']. Use auto to dynamically pick K/M/B/T based on each value's magnitude. |
| format | No | string | This option will format the output value on the results table and CSV export. Supports spreadsheet-style formatting (e.g. #,##0.00). Use this website to help build your custom format. |
| separator | No | string | This option controls the grouping and decimal characters used when rendering numbers (e.g. 1.234.567,50 for European locales). One of: default, commaPeriod, spacePeriod, periodComma, noSeparatorPeriod, apostrophePeriod. |
| groups | No | string or string[] | If you set this property, the dimension will be grouped in the sidebar with other dimensions with the same group label. |
| urls | No | Array of url, label | Adding urls to a dimension allows your users to click dimension values in the UI and take actions, like opening an external tool with a url, or open at a website. You can use liquid templates to customise the link based on the value of the dimension. |
| richText | No | string | Rich text template for displaying formatted content in table cells. Supports Markdown, HTML, and LiquidJS templating. |
| required_attributes | No | Object with user_attribute, value | Limits access to users with those attributes (AND logic - all must match) |
| any_attributes | No | Object with user_attribute, value | Limits access to users with those attributes (OR logic - at least one must match) |
| colors | No | Object with value, color | Color for the values in the chart |
| image | No | Object with url | [WIP] Display images in table cells using URL templates. Supports LiquidJS templating for dynamic URLs. |
| case_sensitive | No | boolean | If set to false, string filters on this dimension will be case insensitive. Defaults to true. Overrides explore-level setting. |
| filter_autocomplete | No | Object | Configure the filter autocomplete suggestions for this dimension. Provide a static list of values (with optional display labels), point at a label_dimension in the same table to label warehouse-fetched values, source values from a dimension in another model with options_from_dimension, and/or disable warehouse-based autocomplete by setting fetch_from_warehouse: false. |
| tags | No | string[] | An array of string tags for categorizing and filtering dimensions programmatically. Tags can be used by AI agents, API filters, and other backend workflows. |
| convert_timezone | No | boolean | [Experimental] If set to false, the dimension opts out of the project query timezone for display, grouping, and extracts - the raw warehouse value is rendered instead. Defaults to true. Has no effect unless a project query timezone is set. |
| timestamp_domain | No | aware or naive | Declares whether a timestamp column stores an instant (aware) or a bare wall clock with no zone (naive). Overrides what Qyra detects from the warehouse catalog. |
Type
The types of your dimensions are pulled from your data warehouse, automatically. You can override these types using the type meta tag in your .yml file. If you run qyra generate to generate your .yml files, then Qyra will add the type from your data warehouse to your .yml files automatically.
- name: user_created_date
config:
meta:
dimension:
type: date- name: user_created_date
meta:
dimension:
type: datedimensions:
- name: user_created_date
type: dateWe currently support these dimension types:
| Dimension Types |
|---|
| string |
| number |
| timestamp |
| date |
| boolean |
Description
Column descriptions in your YAML file are automatically pulled into Qyra and you can spot them if you hover over the dimension name.

Descriptions support any formatting that works with YAML, but the three characters used most often are:
Quotes for escaping
When you surround text with double or single quotes it will escape the text between so that any special characters recognized by YAML will still pass through to the Qyra UI.
description: 'The contents of this column include this & that.'Greater than symbol for folded text blocks
When you use >- it allows you to type descriptions that are multiple lines long in the YAML file, but the text will be combined into a single line when parsed. The qyra generate command will automatically add this to keep YAML files easy to read.
This description in YAML:
- name: product_tier
description: >-
This is a longer description...
...that requires multiple lines
and it will be combined in the Qyra UIWill appear like this in the Qyra UI:
This is a longer description......that requires multiple lines and it will be combined in the Qyra UIVertical bar for preserving line breaks
If you need line breaks to stay in place when they show up in the Qyra UI, you can use a | character like this:
- name: product_tier
description: |
This is a longer description...
...that requires multiple lines
and it will stay on multiple linesAnd in Qyra UI it will appear like this:
This is a longer description...
...that requires multiple lines
and it will stay on multiple linesUsing dbt doc blocks
You can also use dbt docs blocks in descriptions, more on that here.
Format
You can use the format parameter to have your fields show in a particular format in Qyra. Qyra supports spreadsheet-style format expressions for all field types.
To help you build your format expression, we recommend using https://customformats.com/.
models:
- name: sales
columns:
- name: revenue
meta:
metrics:
total_us_revenue:
type: sum
description: 'Total revenue in USD, with two decimal places, compacted to thousands'
format: '$#,##0.00," K"' # 505,430 will appear as '$505.43 K'
percent_of_total_global_revenue:
type: number
description: 'Percent of total global revenue coming from US revenue.'
sql: ${total_us_revenue} / ${total_global_revenue}
format: '0.00%' # 0.67895243 will appear as '67.89%'Example format expressions
| Description | Format Expression | Raw Value | Formatted Output |
|---|---|---|---|
| Adds "km" suffix to the value | #,##0.00" km" | 100000.00 | 100,000.00 km |
| 15000.25 | 15,000.25 km | ||
| 500 | 500.00 km | ||
| Format date with 12-hour clock | m/d/yyyy h:mm AM/PM | 2023-09-05T15:45:00Z | 9/5/2023 3:45 PM |
| 2024-01-20T08:30:00Z | 1/20/2024 8:30 AM | ||
| Display the full name of the day | dddd | 2023-09-05T15:45:00Z | Tuesday |
| 2024-01-20T08:30:00Z | Saturday | ||
| Format positive, negative, and zero values | "⬆️ "0;"⬇️ "0;0 | -500 | ⬇️ 500 |
| 200 | ⬆️ 200 | ||
| 0 | 0 | ||
| Text formatting | "Delivered in "@ | 2 weeks | Delivered in 2 weeks |
| 18 hours | Delivered in 18 hours | ||
| Percentage formatting | #,##0.00% | 0.6758 | 67.58% |
| 0.1 | 10.00% | ||
| 0.002 | 0.20% | ||
| No formatting | 0 | 12345232 | 12345232 |
| 56.7856 | 57 | ||
| Currency formatting (USD) | [$$]#,##0.00 | 15430.75436 | $15,430.75 |
| 1234.50 | $1,234.50 | ||
| Currency formatting (GBP) | [$£]#,##0.00 | 15430.75436 | £15,430.75 |
| 1234.50 | £1,234.50 | ||
| Compact currency in thousands | [$$]#,##0,"K" | 15430.75436 | $15K |
| 15430.75436 | $15.43K | ||
| Compact currency in millions | [$$]#,##0.00,,"M" | 13334567 | $13.33M |
| 120000000 | $120.00M |
Compact
You can compact values in your YAML. For example, if I wanted all of my revenue values to be shown in thousands (e.g. 1,500 appears as 1.50K), then I would write something like this in my .yml:
models:
- name: sales
columns:
- name: revenue
meta:
dimension:
compact: thousands # You can also use 'K'| Value | Alias | Equivalent format expression | Example output |
|---|---|---|---|
| auto | dynamic — picks K/M/B/T per value | 1.50K / 2.30M | |
| thousands | "K" and "thousand" | '#,##0," K"' or '#,##0.00," K"' | 1K |
| millions | "M" and "million" | '#,##0,," M"' or '#,##0.00,," M"' | 1M |
| billions | "B" and "billion" | '#,##0,,," B"' or '#,##0.00,,," B"' | 1B |
| trillions | "T" and "trillion" | '#,##0,,,," T"' or '#,##0.00,,,," T"' | 1T |
| kilobytes | "KB" and "kilobyte" | 1KB | |
| megabytes | "MB" and "megabyte" | 1MB | |
| gigabytes | "GB" and "gigabyte" | 1GB | |
| terabytes | "TB" and "terabyte" | 1TB | |
| petabytes | "PB" and "petabyte" | 1PB | |
| kibibytes | "KiB" and "kibibyte" | 1KiB | |
| mebibytes | "MiB" and "mebibyte" | 1MiB | |
| gibibytes | "GiB" and "gibibyte" | 1GiB | |
| tebibytes | "TiB" and "tebibyte" | 1TiB | |
| pebibytes | "PiB" and "pebibyte" | 1PiB |
Separator
You can use the separator property to control the grouping and decimal characters used when rendering numbers, so values display correctly for non-US locales (e.g. 1.234.567,50 in Europe or 1'234'567.50 in Switzerland).
separator only changes the separator characters. It composes with format (including spreadsheet-style format expressions), compact, round, and currency, which still control the overall shape of the number.
models:
- name: sales
columns:
- name: revenue
meta:
metrics:
total_revenue:
type: sum
format: '[$€]#,##0.00'
separator: periodComma # 1.234.567,50 €These are the supported values:
| Value | Example output | Description |
|---|---|---|
default | 1,234,567.50 | Uses the Qyra default (comma thousands, period decimal). Same as omitting separator. |
commaPeriod | 1,234,567.50 | Comma thousands separator, period decimal separator. |
spacePeriod | 1 234 567.50 | Space thousands separator, period decimal separator. |
periodComma | 1.234.567,50 | Period thousands separator, comma decimal separator. |
noSeparatorPeriod | 1234567.50 | No thousands separator, period decimal separator. |
apostrophePeriod | 1'234'567.50 | Apostrophe thousands separator, period decimal separator (Swiss). |
Time intervals
Qyra automatically adds intervals for dimensions that are timestamps or dates, so you don't have to!
For example, here we have the timestamp dimension created defined in our dbt project:
- name: created
description: 'Timestamp when the user was created.'Qyra breaks this out into the default intervals automatically. So, this is how created appears in our Qyra project:

If you want to apply different formats for different time intervals, we recommend creating additional dimensions for time intervals where you want to customize the format.
Default time intervals
The default time intervals that Qyra adds are...
For date type:
['DAY', 'WEEK', 'MONTH', 'QUARTER', 'YEAR']For timestamp type:
['RAW', 'DAY', 'WEEK', 'MONTH', 'QUARTER', 'YEAR']Disable time intervals
If you want to turn off time intervals for a dimension, you set the time_intervals property to OFF.
In this example, created would now appear as a single, timestamp dimension without a drop-down list of time intervals in Qyra:
- name: created
description: 'Timestamp when the user was created.'
config:
meta:
dimension:
type: timestamp
time_intervals: OFF- name: created
description: 'Timestamp when the user was created.'
meta:
dimension:
type: timestamp
time_intervals: OFFdimensions:
- name: created
description: 'Timestamp when the user was created.'
type: timestamp
time_intervals: OFF
To customize the time intervals for a dimension, you can use the time_intervals parameter.
If you specify time intervals manually, then this overrides the default time intervals used by Qyra.
- name: created
description: 'Timestamp when the user was created.'
config:
meta:
dimension:
time_intervals: ['DAY', 'DAY_OF_MONTH_NUM', 'MONTH', 'QUARTER_NAME', 'YEAR']- name: created
description: 'Timestamp when the user was created.'
meta:
dimension:
time_intervals: ['DAY', 'DAY_OF_MONTH_NUM', 'MONTH', 'QUARTER_NAME', 'YEAR']dimensions:
- name: created
description: 'Timestamp when the user was created.'
time_intervals: ['DAY', 'DAY_OF_MONTH_NUM', 'MONTH', 'QUARTER_NAME', 'YEAR']You can see all of the standard interval options for date and timestamp fields below. You can also add custom intervals using additional dimensions, there's an example below the following tables.
Date options
| Option | Description | Type | Displayed value | Notes |
|---|---|---|---|---|
| RAW | Original value | Date / DateTime | 2019-01-01 / 2019-01-01, 09:30:30:300 UTC | |
| YEAR | Date truncated to the nearest year | Date | 2019 | |
| QUARTER | Date truncated to the nearest quarter | Date | 2019-Q1 | |
| MONTH | Date truncated to the nearest month | Date | 2019-01-01 | |
| WEEK | Date truncated to the nearest week | Date | 2019-01-01 | The start of the week depends on your warehouse configuration |
| DAY | Date truncated to the nearest day | Date | 2019-01-01 | |
| HOUR | Datetime truncated to the nearest hour | DateTime | 2019-01-01, 09 UTC | |
| MINUTE | Datetime truncated to the nearest minute | DateTime | 2019-01-01, 09:30 UTC | |
| SECOND | Datetime truncated to the nearest second | DateTime | 2019-01-01, 09:30:30 UTC | |
| MILLISECOND | Datetime truncated to the nearest millisecond | DateTime | 2019-01-01, 09:30:30:300 UTC |
Numeric options
| Option | Description | Type | Displayed value | Notes |
|---|---|---|---|---|
| DAY_OF_WEEK_INDEX | Index of the day of the week | Number | 0 | The value range and start of the week depends on your warehouse configuration |
| DAY_OF_MONTH_NUM | Day of the month | Number | 21 | |
| DAY_OF_YEAR_NUM | Day of the year | Number | 127 | |
| WEEK_NUM | Week number | Number | 37 | |
| MONTH_NUM | Month number | Number | 7 | |
| QUARTER_NUM | Quarter number | Number | 3 | |
| YEAR_NUM | Year number | Number | 2019 | |
| MINUTE_OF_HOUR_NUM | Minute number | Number | 50 | |
| HOUR_OF_DAY_NUM | Hour number | Number | 22 |
String options
| Option | Description | Type | Displayed value |
|---|---|---|---|
| DAY_OF_WEEK_NAME | Day of the week | String | Monday |
| MONTH_NAME | Month name | String | March |
| QUARTER_NAME | Quarter name | String | Q3 |
Using custom granularities
Beta Custom granularities are available on all plans. What Beta means.
You can define reusable custom time granularities in your qyra.config.yml file and reference them in the time_intervals array. Unlike custom time intervals using additional dimensions, custom granularities appear in the date zoom dropdown alongside standard options (Day, Week, Month, etc.).
Step 1: Define custom granularities in qyra.config.yml:
# qyra.config.yml
custom_granularities:
fiscal_quarter:
label: "Fiscal Quarter"
sql: "DATE_TRUNC('quarter', ${COLUMN} + INTERVAL '1 month')"
week_monday:
label: "Week (Mon-Sun)"
sql: "DATE_TRUNC('week', ${COLUMN})"Step 2: Reference them in your dimension's time_intervals:
- name: order_date
description: 'Date the order was placed'
config:
meta:
dimension:
type: date
time_intervals: ['DAY', 'WEEK', 'fiscal_quarter', 'YEAR']- name: order_date
description: 'Date the order was placed'
meta:
dimension:
type: date
time_intervals: ['DAY', 'WEEK', 'fiscal_quarter', 'YEAR']dimensions:
- name: order_date
description: 'Date the order was placed'
type: date
time_intervals: ['DAY', 'WEEK', 'fiscal_quarter', 'YEAR']The ${COLUMN} placeholder in the SQL expression is automatically replaced with the dimension's column SQL at runtime. Custom granularities also inherit requiredAttributes and anyAttributes from the parent dimension.
See qyra.config.yml reference for the full configuration options and the Date zoom guide for information on configuring the date zoom dropdown.
Custom time intervals with additional dimensions
You can also create custom time-based dimensions by using additional dimensions and groups. This approach groups custom dimensions with their parent date dimension in the sidebar, but these dimensions do not appear in the date zoom dropdown - they are only available as separate fields in the dimension list.
If you need custom time intervals to appear in the date zoom dropdown, use custom granularities defined in qyra.config.yml instead.
Here's an example of how that might look if you wanted to add year_of_week_iso grouped with the delivery_date dimension. Note that by defining the groups: option, we ensure that the new "Year of week" option is displayed grouped with the parent dimension in the sidebar.
- name: delivery_date
config:
meta:
dimension:
label: "Delivery Date"
type: date
time_intervals: [ ... ]
additional_dimensions:
year_of_week_num:
type: number
label: "Year of week"
sql: "yearofweekiso(${delivery_date})"
groups: ["Delivery Date"]- name: delivery_date
meta:
dimension:
label: "Delivery Date"
type: date
time_intervals: [ ... ]
additional_dimensions:
year_of_week_num:
type: number
label: "Year of week"
sql: "yearofweekiso(${delivery_date})"
groups: ["Delivery Date"]dimensions:
- name: delivery_date
label: "Delivery Date"
type: date
time_intervals: [ ... ]
- name: year_of_week_num
type: number
label: "Year of week"
sql: "yearofweekiso(${delivery_date})"
groups: ["Delivery Date"]Reference time intervals in other dimensions
You can reference specific time intervals of a dimension in other dimensions. When you define time intervals for a dimension (like session_start), Qyra creates separate dimensions for each interval (e.g., session_start_day, session_start_month). You can reference these in custom SQL for other dimensions.
For example, if you have a user_created_at dimension with time intervals defined, you can calculate the duration between two dates using the DAY interval:
- name: user_created_at
meta:
dimension:
type: timestamp
time_intervals:
- DAY_OF_WEEK_NAME
- WEEK
- MONTH
- RAW
- DAY
- HOUR_OF_DAY_NUM
- QUARTER
- name: first_purchase_at
meta:
dimension:
type: timestamp
time_intervals:
- DAY
- MONTH
- QUARTER
- name: duration
meta:
dimension:
type: number
sql: EXTRACT(DAY FROM ${first_purchase_at_day} - ${user_created_at_day})In this example, ${user_created_at_day} and ${first_purchase_at_day} reference the DAY time interval versions of the user_created_at and first_purchase_at dimensions.
Groups
You can group your dimensions and metrics in the sidebar using the groups parameter.
To do this, you need to set up group_details in the model's configuration. Then, you can use these groups to organize metrics and dimensions. You can create nested groups up to 3 levels.
models:
- name: baskets
config:
meta:
group_details:
product_details:
label: Product Details
description: 'Fields that have information about the products in the basket.'
item_details:
label: Item Details
description: 'Fields that have information about the items in the basket.'
columns:
- name: basket_item_id
description: 'ID for the product item within the basket.'
config:
meta:
dimension:
groups: ['product_details', 'item_details'] # this would add the dimension to a nested group: `product details` --> `item details`
- name: product_name
description: 'Full name of the product.'
config:
meta:
dimension:
label: 'Product name'
groups: ['product_details'] # this would add the dimension under the group label: `product_details`models:
- name: baskets
meta:
group_details:
product_details:
label: Product Details
description: 'Fields that have information about the products in the basket.'
item_details:
label: Item Details
description: 'Fields that have information about the items in the basket.'
columns:
- name: basket_item_id
description: 'ID for the product item within the basket.'
meta:
dimension:
groups: ['product_details', 'item_details'] # this would add the dimension to a nested group: `product details` --> `item details`
- name: product_name
description: 'Full name of the product.'
meta:
dimension:
label: 'Product name'
groups: ['product_details'] # this would add the dimension under the group label: `product_details`type: model
name: baskets
group_details:
product_details:
label: Product Details
description: 'Fields that have information about the products in the basket.'
item_details:
label: Item Details
description: 'Fields that have information about the items in the basket.'
dimensions:
- name: basket_item_id
description: 'ID for the product item within the basket.'
groups: ['product_details', 'item_details'] # this would add the dimension to a nested group: `product details` --> `item details`
- name: product_name
description: 'Full name of the product.'
label: 'Product name'
groups: ['product_details'] # this would add the dimension under the group label: `product_details`This example would look like this in the sidebar:

URLs
Qyra users can interact with dimension values by clicking on them. If you're already storing URLs in your models, you can create hyperlinks to those URLs in Qyra, like so:
columns:
- name: candidate_profile_url
label: URL of the candidate profile
config:
meta:
dimension:
urls:
- label: Open in CRM
url: ${ value.raw }columns:
- name: candidate_profile_url
label: URL of the candidate profile
meta:
dimension:
urls:
- label: Open in CRM
url: ${ value.raw }dimensions:
- name: candidate_profile_url
label: URL of the candidate profile
urls:
- label: Open in CRM
url: ${ value.raw }How to add custom URLs
By adding custom urls you can configure the actions available to your users. Like linking to external tools, or taking actions in other tools.

In the example below, users can click on a company name and open a corresponding record in their CRM or search for the company in google or open that company's Slack channel.
columns:
- name: company_name
label: Registered trading name of the company
config:
meta:
dimension:
urls:
- label: Search for company in Google
url: 'https://google.com/search?${ value.formatted | url_encode }'
- label: Open in CRM
url: 'https://mycrm.com/companies/${ row.company.company_id.raw | url_encode }'columns:
- name: company_name
label: Registered trading name of the company
meta:
dimension:
urls:
- label: Search for company in Google
url: 'https://google.com/search?${ value.formatted | url_encode }'
- label: Open in CRM
url: 'https://mycrm.com/companies/${ row.company.company_id.raw | url_encode }'dimensions:
- name: company_name
label: Registered trading name of the company
urls:
- label: Search for company in Google
url: 'https://google.com/search?${ value.formatted | url_encode }'
- label: Open in CRM
url: 'https://mycrm.com/companies/${ row.company.company_id.raw | url_encode }'The ${ value.formatted } will be replaced with the value of the company name in the Qyra UI at query run time. The ${ row.company.company_id.raw } will be replaced with the value of the company id in the Qyra UI at query run time. The action will be disabled if the column "company_id" from table "company" is not part of the query.
You can reference values from other columns in your URLs
You can reference another dimension from your table in your URL. For these URLs to work, the other column you've referenced needs to be included in your results table. For example, say I've added a URL to company_name and it uses the field customer_id:
columns:
- name: company_name
label: Registered trading name of the company
config:
meta:
dimension:
urls:
- label: "Open company"
url: "https://example.com/company/${row.customers.customer_id.raw | url_encode }"columns:
- name: company_name
label: Registered trading name of the company
meta:
dimension:
urls:
- label: "Open company"
url: "https://example.com/company/${row.customers.customer_id.raw | url_encode }"dimensions:
- name: company_name
label: Registered trading name of the company
urls:
- label: "Open company"
url: "https://example.com/company/${row.customers.customer_id.raw | url_encode }"This URL will only work if I have customer_id included in my results table.
Liquid templating
Use templates to configure values dynamically at runtime based on query results.
Available liquid tags
| Tag | Description |
|---|---|
${ value.formatted } | The exact value of the dimension as seen in the Qyra UI. For example $1,427.20 |
${ value.raw } | The raw value of the dimension returned from the underlying SQL query. For example 1427.2 |
${ row.table_name.column_name.formatted } | The exact value of the column as seen in the Qyra UI. For example $1,427.20 |
${ row.table_name.column_name.raw } | The raw value of the dimension returned from the underlying SQL query. For example 1427.2 |
Available liquid filters
Filters can be used to make small transformations of your values:
-
url_encode: Encode a string as URL safe, for example it replaces spaces with%20.${ value.formatted | url_encode } -
downcase: Convert a string to lowercase.${ value.formatted | downcase } -
append: Append one string to another.${ value.formatted | append: ".html" }
There are many more filters available in the Liquid documentation.
Rich text
The richText property allows you to define custom HTML/Markdown templates for displaying dimension and metric values in table cells. This enables sophisticated data presentation with formatting, styling, conditional logic, and external integrations.

Rich text only renders in the Table chart visualization. It does not affect the Results panel below the explore, CSV/Excel exports, or the underlying data values. To see your richText take effect, switch the chart type to Table.
richText is supported on both dimensions and metrics. It accepts Markdown, inline HTML, and LiquidJS templating. HTML is sanitized with a GitHub-safe allowlist that permits inline style attributes but strips <script>, <iframe>, and event handlers (e.g. onclick). External links automatically open in a new tab.
Basic example
Display a dimension value with bold text, an inline code span, and clickable links:
columns:
- name: customer_id
description: 'Unique customer identifier'
config:
meta:
dimension:
richText: |
**Customer ID:** `${ value.raw }`
[View Profile](https://example.com/customer/${ value.raw }) | [Search Google](https://google.com/search?q=customer%20${ value.formatted | url_encode })columns:
- name: customer_id
description: 'Unique customer identifier'
meta:
dimension:
richText: |
**Customer ID:** `${ value.raw }`
[View Profile](https://example.com/customer/${ value.raw }) | [Search Google](https://google.com/search?q=customer%20${ value.formatted | url_encode })dimensions:
- name: customer_id
description: 'Unique customer identifier'
richText: |
**Customer ID:** `${ value.raw }`
[View Profile](https://example.com/customer/${ value.raw }) | [Search Google](https://google.com/search?q=customer%20${ value.formatted | url_encode })
Conditional formatting
Use Liquid control flow tags to switch rendering based on the value — for example, to tier a metric with emoji indicators and a matching text label. Always guard against nil so empty cells render gracefully:
columns:
- name: customer_lifetime_value
config:
meta:
metrics:
average_clv:
type: average
richText: |
{% raw %}{% if value.raw == nil %}
<span style="color: #999;">No data</span>
{% elsif value.raw >= 100 %}
### 📈 ${ value.formatted }
**Excellent** Average CLV
{% elsif value.raw >= 50 %}
### 📊 ${ value.formatted }
**Good** Average CLV
{% else %}
### ⚠️ ${ value.formatted }
**Low** Average CLV - _Needs attention!_
{% endif %}{% endraw %}columns:
- name: customer_lifetime_value
meta:
metrics:
average_clv:
type: average
richText: |
{% raw %}{% if value.raw == nil %}
<span style="color: #999;">No data</span>
{% elsif value.raw >= 100 %}
### 📈 ${ value.formatted }
**Excellent** Average CLV
{% elsif value.raw >= 50 %}
### 📊 ${ value.formatted }
**Good** Average CLV
{% else %}
### ⚠️ ${ value.formatted }
**Low** Average CLV - _Needs attention!_
{% endif %}{% endraw %}metrics:
- name: average_clv
type: average
sql: ${customer_lifetime_value}
richText: |
{% raw %}{% if value.raw == nil %}
<span style="color: #999;">No data</span>
{% elsif value.raw >= 100 %}
### 📈 ${ value.formatted }
**Excellent** Average CLV
{% elsif value.raw >= 50 %}
### 📊 ${ value.formatted }
**Good** Average CLV
{% else %}
### ⚠️ ${ value.formatted }
**Low** Average CLV - _Needs attention!_
{% endif %}{% endraw %}
Liquid blocks inside dbt YAML must be wrapped in {% raw %}...{% endraw %} so dbt's Jinja engine passes the template through to Qyra untouched.
HTML, images, and inline styles
For richer layouts, combine inline HTML with images and style attributes. This example renders an avatar image alongside the name with a secondary link underneath:
columns:
- name: full_name
description: 'Customer full name'
config:
meta:
dimension:
type: string
richText: |
{% raw %}{% if value.raw == nil %}
<span style="color: #999;">-</span>
{% else %}
<div style="display: flex; align-items: center; gap: 8px;">
<img src="https://ui-avatars.com/api/?name=${ value.formatted | url_encode }&background=random&size=32&rounded=true"
width="32" height="32"
style="border-radius: 50%;"
alt="${ value.formatted }" />
<p>
<strong style="float:left">${ value.formatted }</strong><br/>
<span style="color: #666; font-size: 0.9em;">
<a style="float:left;" href="https://www.linkedin.com/search/results/people/?keywords=${ value.formatted | url_encode }" target="_blank">LinkedIn</a>
</span>
</p>
</div>
{% endif %}{% endraw %}columns:
- name: full_name
description: 'Customer full name'
meta:
dimension:
type: string
richText: |
{% raw %}{% if value.raw == nil %}
<span style="color: #999;">-</span>
{% else %}
<div style="display: flex; align-items: center; gap: 8px;">
<img src="https://ui-avatars.com/api/?name=${ value.formatted | url_encode }&background=random&size=32&rounded=true"
width="32" height="32"
style="border-radius: 50%;"
alt="${ value.formatted }" />
<p>
<strong style="float:left">${ value.formatted }</strong><br/>
<span style="color: #666; font-size: 0.9em;">
<a style="float:left;" href="https://www.linkedin.com/search/results/people/?keywords=${ value.formatted | url_encode }" target="_blank">LinkedIn</a>
</span>
</p>
</div>
{% endif %}{% endraw %}dimensions:
- name: full_name
description: 'Customer full name'
type: string
richText: |
{% raw %}{% if value.raw == nil %}
<span style="color: #999;">-</span>
{% else %}
<div style="display: flex; align-items: center; gap: 8px;">
<img src="https://ui-avatars.com/api/?name=${ value.formatted | url_encode }&background=random&size=32&rounded=true"
width="32" height="32"
style="border-radius: 50%;"
alt="${ value.formatted }" />
<p>
<strong style="float:left">${ value.formatted }</strong><br/>
<span style="color: #666; font-size: 0.9em;">
<a style="float:left;" href="https://www.linkedin.com/search/results/people/?keywords=${ value.formatted | url_encode }" target="_blank">LinkedIn</a>
</span>
</p>
</div>
{% endif %}{% endraw %}
Liquid templating in rich text
Use templates to configure values dynamically at runtime based on query results.
Available liquid tags
| Tag | Description |
|---|---|
${ value.formatted } | The exact value of the dimension as seen in the Qyra UI. For example $1,427.20 |
${ value.raw } | The raw value of the dimension returned from the underlying SQL query. For example 1427.2 |
${ row.table_name.column_name.formatted } | The exact value of the column as seen in the Qyra UI. For example $1,427.20 |
${ row.table_name.column_name.raw } | The raw value of the dimension returned from the underlying SQL query. For example 1427.2 |
Available liquid filters
Filters can be used to make small transformations of your values:
-
url_encode: Encode a string as URL safe, for example it replaces spaces with%20.${ value.formatted | url_encode } -
downcase: Convert a string to lowercase.${ value.formatted | downcase } -
append: Append one string to another.${ value.formatted | append: ".html" }
There are many more filters available in the Liquid documentation.
Required attributes
Qyra can use user attributes to limit some dimensions to some users.
In the example below, only users with is_admin attribute true can use the salary dimension on user table. Users without access to this dimension will not see it or the custom metrics created from this dimension on the explore page.
columns:
- name:
description: User name
- salary:
description: User salary
config:
meta:
dimension:
required_attributes:
is_admin: "true"columns:
- name:
description: User name
- salary:
description: User salary
meta:
dimension:
required_attributes:
is_admin: "true"dimensions:
- name: user_name
description: User name
- name: salary
description: User salary
required_attributes:
is_admin: "true"If a user without access to this dimension runs a query that contains this dimension, they will get a Forbidden error.
Any attributes
While required_attributes uses AND logic (all conditions must match), any_attributes uses OR logic — a user only needs to match at least one condition.
columns:
- name: salary
description: User salary
config:
meta:
dimension:
any_attributes:
department: ["hr", "finance"]
role: "manager"columns:
- name: salary
description: User salary
meta:
dimension:
any_attributes:
department: ["hr", "finance"]
role: "manager"dimensions:
- name: salary
description: User salary
any_attributes:
department: ["hr", "finance"]
role: "manager"In this example, users with department = "hr" OR department = "finance" OR role = "manager" can see the salary dimension.
You can combine both required_attributes and any_attributes on the same dimension. When both are set, both checks must pass. See user attributes for details.
Current limitations
Qyra dimensions and custom metrics are protected by this feature, however, it is possible to write custom SQL to bypass this filter, for example:
- Developers and admins running SQL queries on SQL runner.
- Custom SQL or subqueries on
table calculations
Scheduler deliveries will run against the user who created the scheduled delivery, be careful when sharing required attributes with other users.
Convert timezone
Experimental Contact support to enable the convert_timezone dimension override for your organization. It has no effect unless the project query timezone is set. What Experimental means.
Set convert_timezone: false on a timestamp dimension to opt it out of the project query timezone. This is useful for columns that should always render in their raw warehouse value - system timestamps, audit logs, or pre-converted timezone columns.
When set, the dimension's display, grouping, and calendar extracts all use the raw warehouse value. Time-interval children of the dimension (_day, _month, _day_of_week_index, ...) inherit the opt-out automatically.
Filters still use the project query timezone regardless of this setting, so every dimension on a row agrees on whether the row matches the filter.
columns:
- name: created_at_utc
config:
meta:
dimension:
type: timestamp
convert_timezone: falsecolumns:
- name: created_at_utc
meta:
dimension:
type: timestamp
convert_timezone: falsedimensions:
- name: created_at_utc
type: timestamp
convert_timezone: falseSee Per-dimension timezone opt-out in the Timezones guide for how this interacts with other timezone settings and filter behavior.
Timestamp domain
Qyra reads whether a timestamp column stores an instant (aware) or a bare wall clock (naive) from your warehouse catalog, so you don't usually need to set this. Override it when the catalog type doesn't match the data, for example a TIMESTAMP_NTZ column that already holds UTC instants.
columns:
- name: created_at
config:
meta:
dimension:
type: timestamp
timestamp_domain: awarecolumns:
- name: created_at
meta:
dimension:
type: timestamp
timestamp_domain: awaredimensions:
- name: created_at
type: timestamp
timestamp_domain: awareThe YAML value wins over the catalog. It's ignored on dimensions with custom sql and on additional dimensions. Interval children (_day, _month, ...) inherit from their base.
See Override a column timestamp domain in the Timezones guide.
Color
You can predefine colors for your string type dimensions, these colors will be used instead of your default organization colors for the right value when you use a grouped bar chart or a pie chart.
- name: status
description: "{{ doc(\"orders_status\") }}"
config:
meta:
dimension:
colors:
"placed": "#e6fa0f"
"completed": "#558B2F"
"shipped": "#29B6F6"
"return_pending": "#FF6F00"
"returned": "#E91E63" - name: status
description: "{{ doc(\"orders_status\") }}"
meta:
dimension:
colors:
"placed": "#e6fa0f"
"completed": "#558B2F"
"shipped": "#29B6F6"
"return_pending": "#FF6F00"
"returned": "#E91E63"dimensions:
- name: status
description: "Order status"
colors:
"placed": "#e6fa0f"
"completed": "#558B2F"
"shipped": "#29B6F6"
"return_pending": "#FF6F00"
"returned": "#E91E63"
Use #HEX colors. Other color types like rgb, rgba, or a color name (for example, orange) work on charts, but not in the chart config.
You can override a dimension color by picking a color for that series in the chart config, and that color takes precedence over the organization color palette.
Case sensitive
You can control whether string filters on a dimension are case sensitive or case insensitive.
- Default:
case_sensitive: true(case sensitive) - When
false: string filters ignore case differences
This setting affects these string filter operators:
equalsnot equalsstarts withends withincludesdoesn't include
columns:
- name: customer_name
description: 'Name of the customer'
config:
meta:
dimension:
case_sensitive: falsecolumns:
- name: customer_name
description: 'Name of the customer'
meta:
dimension:
case_sensitive: falsedimensions:
- name: customer_name
description: 'Name of the customer'
case_sensitive: falseSQL behavior examples
Given a filter value jes on a dimension like customers.first_name:
- With
case_sensitive: true(default), generated SQL uses case-sensitive matching, for example:
("customers"."first_name") LIKE '%jes%'- With
case_sensitive: false, generated SQL normalizes both sides for case-insensitive matching:
UPPER("customers"."first_name") LIKE UPPER('%jes%')So with case_sensitive: false, filtering for john would match John, JOHN, john, etc.
Dimension-level case_sensitive settings override explore-level and project-level settings. See Tables reference for explore-level configuration and qyra.config.yml reference for project-level defaults.
Filter autocomplete
You can customize the suggestions users see when filtering on a dimension. This is useful when you want to:
- Provide a curated, static list of suggested values (with optional human-friendly labels) instead of relying solely on values from your warehouse.
- Label warehouse-fetched values with a human-readable name from another dimension in the same table (for example, showing customer names next to customer IDs).
- Source autocomplete values from a dimension in another model (for example, drive a
flight.airline_codefilter from the canonical list indim_airlines). - Disable the live warehouse lookup for autocomplete to reduce warehouse queries on dimensions with very high cardinality or sensitive values.
Regardless of which option you use, filter autocomplete always returns { value, label } pairs. The value is what gets used in the filter, and the label is what's displayed in the dropdown. Parameters that source their options from the same dimension via options_from_dimension automatically pick up these labels too.
Properties
| Property | Required | Value | Description |
|---|---|---|---|
| values | No | Array of value, label | A static, hard-coded list of suggested filter values. value is the raw value used in the filter; label is the optional display name shown in the autocomplete dropdown. Duplicate values are ignored (the first occurrence wins). Use this when the set of values is small and known ahead of time. |
| label_dimension | No | string | Name of another dimension in the same table whose value is used as the display label for each warehouse-fetched value. Unlike static values (which only supports hard-coded labels), label_dimension labels every value that comes back from the warehouse, and search and sort both run against the label rather than the underlying value. Ignored when options_from_dimension is set. |
| options_from_dimension | No | Object | Fetch autocomplete values from a dimension in another model instead of from this dimension itself. Requires model and dimension; accepts an optional label_dimension in that source model. Cascading filters from the current explore are not applied to the lookup query. Ignored when fetch_from_warehouse: false is also set. |
| fetch_from_warehouse | No | boolean | If set to false, Qyra won't query the warehouse for autocomplete suggestions on this dimension. Defaults to true. |
Static suggested values with labels
columns:
- name: status
description: 'Subscription status'
config:
meta:
dimension:
filter_autocomplete:
values:
- value: 'active'
label: 'Active customer'
- value: 'trial'
label: 'Trial'
- value: 'churned'columns:
- name: status
description: 'Subscription status'
meta:
dimension:
filter_autocomplete:
values:
- value: 'active'
label: 'Active customer'
- value: 'trial'
label: 'Trial'
- value: 'churned'dimensions:
- name: status
description: 'Subscription status'
filter_autocomplete:
values:
- value: 'active'
label: 'Active customer'
- value: 'trial'
label: 'Trial'
- value: 'churned'Disabling warehouse autocomplete
Set fetch_from_warehouse: false to stop Qyra from querying your warehouse for autocomplete suggestions on this dimension. Users can still type values manually; if you also provide values, those will be shown as suggestions.
columns:
- name: customer_email
config:
meta:
dimension:
filter_autocomplete:
fetch_from_warehouse: falsecolumns:
- name: customer_email
meta:
dimension:
filter_autocomplete:
fetch_from_warehouse: falsedimensions:
- name: customer_email
filter_autocomplete:
fetch_from_warehouse: falseLabelling warehouse-fetched values with another dimension
Set label_dimension to the name of another dimension in the same table to show a human-readable label alongside each warehouse-fetched value. The filter still applies to the underlying value, but the dropdown displays the label. Search and sort in the autocomplete dropdown also run against the label rather than the underlying value.
For example, an orders model exposes a customer_id dimension that users want to filter on, but the ids are opaque. Adding label_dimension: customer_name makes the autocomplete show customer names in the dropdown while still filtering by customer_id:
columns:
- name: customer_id
config:
meta:
dimension:
filter_autocomplete:
label_dimension: customer_name
- name: customer_name
config:
meta:
dimension: {}columns:
- name: customer_id
meta:
dimension:
filter_autocomplete:
label_dimension: customer_name
- name: customer_name
meta:
dimension: {}dimensions:
- name: customer_id
filter_autocomplete:
label_dimension: customer_name
- name: customer_nameThe referenced dimension must live in the same table. Any parameter that sources its options from customer_id via options_from_dimension also picks up the customer name labels automatically.
Sourcing values from another model
Set options_from_dimension to fetch autocomplete values from a dimension in another model instead of from this dimension itself. This is useful when the canonical list of allowed values lives in a lookup or dimension table, but the filter is exposed on a fact table that only stores foreign keys.
For example, an flights model exposes an airline_code dimension, but the full list of airlines lives in a dim_airlines lookup model. Point options_from_dimension at dim_airlines.code to drive the autocomplete from the lookup, and add label_dimension: name to show the airline name next to each code:
columns:
- name: airline_code
config:
meta:
dimension:
filter_autocomplete:
options_from_dimension:
model: dim_airlines
dimension: code
label_dimension: namecolumns:
- name: airline_code
meta:
dimension:
filter_autocomplete:
options_from_dimension:
model: dim_airlines
dimension: code
label_dimension: namedimensions:
- name: airline_code
filter_autocomplete:
options_from_dimension:
model: dim_airlines
dimension: code
label_dimension: nameProperties inside options_from_dimension:
| Property | Required | Value | Description |
|---|---|---|---|
| model | Yes | string | The model to source autocomplete values from. Must have an explore — hidden models and seeds can't be used. |
| dimension | Yes | string | The dimension in model whose values populate the autocomplete dropdown. |
| label_dimension | No | string | A dimension in the same source model whose value is shown as the display label for each fetched value. Search and sort run against the label. |
Notes:
- Cascading filters from the current explore are not applied to the lookup query, so users see the full list of values from the source model regardless of other filters on the current query.
- The dimension-level
label_dimensionon the current dimension is ignored whenoptions_from_dimensionis set. Setlabel_dimensioninsideoptions_from_dimensioninstead — labels always come from the model the values come from. - If
fetch_from_warehouse: falseis also set on this dimension, curatedvaluesare served andoptions_from_dimensionis ignored. Qyra surfaces a project warning when both are configured together.
Image Display
Work in Progress: This feature is currently in development and may be subject to changes.
You can configure dimensions to display images in table cells using URL templates. Images are rendered as thumbnails with hover tooltips showing larger previews, perfect for visualizing product images, user avatars, or any other image-based data.
How it works
To display images, you define an image property with a URL template in your dimension or additional dimension configuration. The URL can be dynamic, using LiquidJS templating to construct URLs based on row data and field values.
Simplest example - column already contains image URLs:
If your column already contains complete image URLs, you can simply configure it to display as an image:
columns:
- name: image_url
description: "Image URL for the event"
config:
meta:
dimension:
type: string
image:
url: "${value.raw}"columns:
- name: image_url
description: "Image URL for the event"
meta:
dimension:
type: string
image:
url: "${value.raw}"dimensions:
- name: image_url
description: "Image URL for the event"
type: string
image:
url: "${value.raw}"With custom dimensions and fit:
You can control the size and display behavior of images using width, height, and fit options:
columns:
- name: product_image_url
description: "Product image URL"
config:
meta:
dimension:
type: string
image:
url: "${value.raw}"
width: 100
height: 100
fit: "cover"columns:
- name: product_image_url
description: "Product image URL"
meta:
dimension:
type: string
image:
url: "${value.raw}"
width: 100
height: 100
fit: "cover"dimensions:
- name: product_image_url
description: "Product image URL"
type: string
image:
url: "${value.raw}"
width: 100
height: 100
fit: "cover"Basic example with URL template:
columns:
- name: product_id
config:
meta:
additional_dimensions:
product_image:
type: string
label: "Product Image"
description: "Product thumbnail image"
image:
url: "https://example.com/images/${value.raw}.jpg"columns:
- name: product_id
meta:
additional_dimensions:
product_image:
type: string
label: "Product Image"
description: "Product thumbnail image"
image:
url: "https://example.com/images/${value.raw}.jpg"dimensions:
- name: product_id
- name: product_image
type: string
label: "Product Image"
description: "Product thumbnail image"
image:
url: "https://example.com/images/${value.raw}.jpg"Advanced example with dynamic URLs:
columns:
- name: event_type
config:
meta:
additional_dimensions:
event_image:
type: string
label: "Event Image"
description: "Event banner image"
image:
url: "https://cdn.example.com/${value.raw}-${row.events.event_id.raw | upcase}.png"columns:
- name: event_type
meta:
additional_dimensions:
event_image:
type: string
label: "Event Image"
description: "Event banner image"
image:
url: "https://cdn.example.com/${value.raw}-${row.events.event_id.raw | upcase}.png"dimensions:
- name: event_type
- name: event_image
type: string
label: "Event Image"
description: "Event banner image"
image:
url: "https://cdn.example.com/${value.raw}-${row.events.event_id.raw | upcase}.png"Display behavior
- Thumbnails: Images display as thumbnails in table cells (default: 32px height)
- Hover preview: Hovering over a thumbnail reveals a larger preview via tooltip
- Error handling: Failed image loads show a photo-off icon with error details in the tooltip
- Validation: Invalid URL syntax is caught and displays an error state
Image configuration properties
The image object supports the following properties:
| Property | Type | Default | Description |
|---|---|---|---|
url | string | (required) | The image URL. Supports LiquidJS templating with ${value.raw}, ${row.table.column.raw}, etc. |
width | number | auto | Image width in pixels. When specified without height, height defaults to 'auto' to maintain aspect ratio. |
height | number | 32 | Image height in pixels. |
fit | string | "cover" | Controls how the image fits within the dimensions. Options: "cover", "contain", "fill", or "none". |
Fit options:
cover(default) - Image fills the space while maintaining aspect ratio. May crop content.contain- Scales image to fit within dimensions while preserving aspect ratio. No cropping.fill- Stretches image to fill dimensions. May distort aspect ratio.none- Displays image at its original size without scaling.
Examples:
# Square thumbnail with cover fit
image:
url: "${value.raw}"
width: 50
height: 50
fit: "cover"
# Wide thumbnail maintaining aspect ratio
image:
url: "https://example.com/images/${value.raw}.jpg"
width: 100
# height defaults to 'auto' to maintain aspect ratio
# Original size display
image:
url: "${value.raw}"
fit: "none"URL Templating
The image.url property supports LiquidJS templating with the same tags and filters available for dimension URLs:
Available liquid tags:
| Tag | Description |
|---|---|
${ value.formatted } | The exact value of the dimension as seen in the Qyra UI |
${ value.raw } | The raw value of the dimension returned from the underlying SQL query |
${ row.table_name.column_name.formatted } | The exact value of another column as seen in the Qyra UI |
${ row.table_name.column_name.raw } | The raw value of another dimension returned from the underlying SQL query |
Available liquid filters:
You can use liquid filters to transform values:
image:
url: "https://example.com/${value.raw | downcase}.jpg"Current limitations
- Dimensions only: Image display is currently supported for dimensions and additional dimensions only (not metrics or table calculations)
- Referenced columns: When using
${ row.table_name.column_name }templates, the referenced column must be included in your results table for the image to display
Example: Product catalog with images
models:
- name: products
columns:
- name: sku
description: "Product SKU code"
config:
meta:
dimension:
type: string
additional_dimensions:
product_thumbnail:
type: string
label: "Product Image"
description: "Product catalog image"
image:
url: "https://cdn.mystore.com/products/${value.raw}/thumbnail.jpg"
width: 80
height: 80
fit: "cover"
- name: category
description: "Product category"
config:
meta:
dimension:
type: string
additional_dimensions:
category_icon:
type: string
label: "Category Icon"
description: "Category icon image"
image:
url: "https://cdn.mystore.com/icons/${value.raw | downcase}.png"
width: 40
height: 40
fit: "contain"
- name: hero_image_url
description: "Product hero image URL"
config:
meta:
dimension:
type: string
image:
url: "${value.raw}"
width: 120
fit: "cover"models:
- name: products
columns:
- name: sku
description: "Product SKU code"
meta:
dimension:
type: string
additional_dimensions:
product_thumbnail:
type: string
label: "Product Image"
description: "Product catalog image"
image:
url: "https://cdn.mystore.com/products/${value.raw}/thumbnail.jpg"
width: 80
height: 80
fit: "cover"
- name: category
description: "Product category"
meta:
dimension:
type: string
additional_dimensions:
category_icon:
type: string
label: "Category Icon"
description: "Category icon image"
image:
url: "https://cdn.mystore.com/icons/${value.raw | downcase}.png"
width: 40
height: 40
fit: "contain"
- name: hero_image_url
description: "Product hero image URL"
meta:
dimension:
type: string
image:
url: "${value.raw}"
width: 120
fit: "cover"type: model
name: products
dimensions:
- name: sku
description: "Product SKU code"
type: string
- name: product_thumbnail
type: string
label: "Product Image"
description: "Product catalog image"
image:
url: "https://cdn.mystore.com/products/${value.raw}/thumbnail.jpg"
width: 80
height: 80
fit: "cover"
- name: category
description: "Product category"
type: string
- name: category_icon
type: string
label: "Category Icon"
description: "Category icon image"
image:
url: "https://cdn.mystore.com/icons/${value.raw | downcase}.png"
width: 40
height: 40
fit: "contain"
- name: hero_image_url
description: "Product hero image URL"
type: string
image:
url: "${value.raw}"
width: 120
fit: "cover"Tags
You can add tags to individual dimensions to categorize them for programmatic use. Tags are string arrays that Qyra preserves and exposes via the API.
Tags are useful for:
- AI agent access control — restrict which dimensions an AI agent can see
- API filtering — filter the data catalog metrics endpoint by tag to retrieve only the dimensions you need
- Pipeline ingestion — flag specific dimensions for downstream ETL or semantic layer workflows
Tags are not displayed as Metrics Catalog categories in the Qyra UI. They function as a backend/programmatic control mechanism only.
models:
- name: orders
columns:
- name: status
config:
meta:
dimension:
tags: ["core", "sales"]
- name: location
config:
meta:
dimension:
tags: ["core", "operations"]models:
- name: orders
columns:
- name: status
meta:
dimension:
tags: ["core", "sales"]
- name: location
meta:
dimension:
tags: ["core", "operations"]type: model
name: orders
dimensions:
- name: status
tags: ["core", "sales"]
- name: location
tags: ["core", "operations"]Using special characters or capital letters in your column names
If you use special characters on your column names, you might get errors when using those columns on explore. For example, having a column named Status with capital S on a table named orders in postgres throws the following error:
column orders.status does not existTo fix this, we can add the quoted column to our sql meta tag on dimensions
- name: status
config:
meta:
dimension:
type: string
sql: '"orders"."Status"' # you can also use '${TABLE}."Status"'- name: status
meta:
dimension:
type: string
sql: '"orders"."Status"' # you can also use '${TABLE}."Status"'dimensions:
- name: status
type: string
sql: '"orders"."Status"' # you can also use '${TABLE}."Status"'This will quote the Status columns on the SQL query
SELECT
"orders".order_id AS "orders_order_id",
"orders"."Status" AS "orders_status"
FROM "postgres"."jaffle"."orders" AS "orders"Additional dimensions
Additional dimensions let you define multiple dimensions off of a single column from your dbt model. This is useful when adding different formatting to a column, comparing or combining columns, parsing JSON columns, or creating persisted groups/buckets based off of a column.
A "normal" dimension is a column created in your .sql file in dbt that is written to your data warehouse. An additional dimension is not included in your dbt .sql file, so it's not written to your data warehouse. When used in Qyra, it just adds the dimension definition to your SQL query (so it's "created" at runtime).
All dimension configurations are available for additional dimensions. You can also use additional dimensions when defining metrics.
Additional dimensions names need to be unique in the model.
Adding different formatting
columns:
- name: revenue
config:
meta:
dimension:
type: number
additional_dimensions:
revenue_in_thousands:
type: number
format: '#,##0," K"'
revenue_in_millions:
type: number
format: '#,##0,," M"'columns:
- name: revenue
meta:
dimension:
type: number
additional_dimensions:
revenue_in_thousands:
type: number
format: '#,##0," K"'
revenue_in_millions:
type: number
format: '#,##0,," M"'dimensions:
- name: revenue
type: number
- name: revenue_in_thousands
type: number
sql: ${TABLE}.revenue
format: '#,##0," K"'
- name: revenue_in_millions
type: number
sql: ${TABLE}.revenue
format: '#,##0,," M"'Comparing or combining columns
When defining additional dimensions, you can reference other dimensions, even from joined tables (organizations is a joined table in the example below).
columns:
- name: created_date
config:
meta:
dimension:
type: date
additional_dimensions:
days_to_first_query_run:
type: number
description: 'Number of days between a user being created and their first query run.'
sql: ${first_query_date} - ${created_date}
days_to_organization_first_payment:
type: number
description: 'Number of days between a user being created and their organization making its first payment. This will be negative for users who joined after the first payment.'
sql: ${created_date} - ${organizations.first_payment_date}columns:
- name: created_date
meta:
dimension:
type: date
additional_dimensions:
days_to_first_query_run:
type: number
description: 'Number of days between a user being created and their first query run.'
sql: ${first_query_date} - ${created_date}
days_to_organization_first_payment:
type: number
description: 'Number of days between a user being created and their organization making its first payment. This will be negative for users who joined after the first payment.'
sql: ${created_date} - ${organizations.first_payment_date}dimensions:
- name: created_date
type: date
- name: days_to_first_query_run
type: number
description: 'Number of days between a user being created and their first query run.'
sql: ${first_query_date} - ${created_date}
- name: days_to_organization_first_payment
type: number
description: 'Number of days between a user being created and their organization making its first payment. This will be negative for users who joined after the first payment.'
sql: ${created_date} - ${organizations.first_payment_date}Parsing JSON columns
Usually you'll want to add hidden:true for the main JSON dimension since raw JSON is not useful in charts.
columns:
- name: metadata # this is a jsonb column with metadata
config:
meta:
dimension:
hidden: true
additional_dimensions:
version:
type: number
sql: JSON_VALUE(${metadata}, '$.version') # custom SQL applied to get the "version" value inside metadatacolumns:
- name: metadata # this is a jsonb column with metadata
meta:
dimension:
hidden: true
additional_dimensions:
version:
type: number
sql: JSON_VALUE(${metadata}, '$.version') # custom SQL applied to get the "version" value inside metadatadimensions:
- name: metadata # this is a jsonb column with metadata
hidden: true
- name: version
type: number
sql: JSON_VALUE(${metadata}, '$.version') # custom SQL applied to get the "version" value inside metadataAdding multiple timezones for the same dimension
You can use additional dimensions to convert a timestamp into multiple timezones:
columns:
- name: created_at
description: 'The time that the thing was created'
config:
meta:
dimension:
label: 'Created (UTC)'
type: timestamp
additional_dimensions:
created_at_est:
type: timestamp
label: 'Created (EST)'
description: 'The time that the thing was created, in EST'
sql: "convert_timezone('UTC', 'America/New_York', ${TABLE}.created_at)"columns:
- name: created_at
description: 'The time that the thing was created'
meta:
dimension:
label: 'Created (UTC)'
type: timestamp
additional_dimensions:
created_at_est:
type: timestamp
label: 'Created (EST)'
description: 'The time that the thing was created, in EST'
sql: "convert_timezone('UTC', 'America/New_York', ${TABLE}.created_at)"dimensions:
- name: created_at
description: 'The time that the thing was created'
label: 'Created (UTC)'
type: timestamp
- name: created_at_est
type: timestamp
label: 'Created (EST)'
description: 'The time that the thing was created, in EST'
sql: "convert_timezone('UTC', 'America/New_York', ${TABLE}.created_at)"Using additional dimensions in metrics
To define metrics based on additional dimensions, you need to add them to the model's meta metrics, or use custom SQL in defining them under the column's meta.
models:
- name: users
config:
meta:
metrics:
highest_version_model_metric_example:
type: max
sql: ${version}
columns:
- name: metadata
meta:
dimension:
hidden: true
additional_dimensions:
version:
type: number
sql: JSON_VALUE(${metadata}, '$.version')
metrics:
highest_version:
type: max
sql: ${version}models:
- name: users
meta:
metrics:
highest_version_model_metric_example:
type: max
sql: ${version}
columns:
- name: metadata
meta:
dimension:
hidden: true
additional_dimensions:
version:
type: number
sql: JSON_VALUE(${metadata}, '$.version')
metrics:
highest_version:
type: max
sql: ${version}type: model
name: users
dimensions:
- name: metadata
hidden: true
- name: version
type: number
sql: JSON_VALUE(${metadata}, '$.version')
metrics:
highest_version_model_metric_example:
type: max
sql: ${version}
highest_version:
type: max
sql: ${version}Referencing time intervals in additional dimensions
You can reference time interval dimensions within additional dimensions. This is useful for creating custom logic based on specific time intervals.
For example, you can create a tier based on whether a specific time interval has a value:
- name: session_start
config:
meta:
dimension:
type: timestamp
time_intervals:
- DAY_OF_WEEK_NAME
- WEEK
- MONTH
- RAW
- DAY
- HOUR_OF_DAY_NUM
- QUARTER
additional_dimensions:
extra_session_start_tier:
type: string
sql: |
CASE
WHEN ${session_start_month} IS NOT NULL THEN 'month'
ELSE 'else' END - name: session_start
meta:
dimension:
type: timestamp
time_intervals:
- DAY_OF_WEEK_NAME
- WEEK
- MONTH
- RAW
- DAY
- HOUR_OF_DAY_NUM
- QUARTER
additional_dimensions:
extra_session_start_tier:
type: string
sql: |
CASE
WHEN ${session_start_month} IS NOT NULL THEN 'month'
ELSE 'else' ENDdimensions:
- name: session_start
type: timestamp
time_intervals:
- DAY_OF_WEEK_NAME
- WEEK
- MONTH
- RAW
- DAY
- HOUR_OF_DAY_NUM
- QUARTER
- name: extra_session_start_tier
type: string
sql: |
CASE
WHEN ${session_start_month} IS NOT NULL THEN 'month'
ELSE 'else' ENDIn this example, the additional dimension extra_session_start_tier references ${session_start_month}, which is the MONTH time interval of the session_start dimension.