Qyra

Tables reference

How dbt models become queryable Tables, and every option for configuring them

Tables in Qyra are built from dbt models (either one, or many joined together).

Adding Tables to your project

Tables come from dbt models that have been defined in your dbt project's schema.yml files.

If your dbt model has been defined in a YAML file, and has at least one column documented, it will appear in Qyra as a table.

For example, if we had this in our schema.yml files in dbt, we'd see a Table called Users in Qyra.

models:
  - name: users
    columns:
      - name: user_id
        description: "The unique identified for each user"

The build your semantic layer track walks through generating this YAML with the Qyra CLI.

Choosing which Tables appear in Qyra

Not every dbt model with a YAML file belongs in Qyra — staging and intermediate models usually shouldn't be queryable by end users. Tables configuration in your project settings controls which models become Tables.

To reach it, click the gear icon in the top-right navigation bar, choose Project settings, then select Tables configuration in the left sidebar. The setting is only visible to users whose role lets them edit project settings.

OptionBehavior
Show entire projectEvery model with a YAML file in your dbt project appears as a Table.
Show models with any of these tagsOnly models carrying one of the dbt tags you list appear as Tables.
Show models in this listOnly the models you select by name appear as Tables.
Tables configuration in Qyra project settings

Tables configuration filters models after Qyra compiles them. To skip compilation for models entirely — which speeds up syncing on large projects — use the dbt selector in your project settings instead.

Limiting Tables with dbt tags

Tagging is the most maintainable way to control which models appear, because the decision lives in your dbt project alongside the model rather than in a list someone has to remember to update. Use a set of tags you already maintain, or create a Qyra-specific one.

Add the tag in your model's YAML file:

models:
  - name: model_name
    config:
      tags: ['qyra']
models:
  - name: model_name
    tags: ['qyra']
type: model
name: model_name

tags: ['qyra']

Or in the model's SQL file, in the config block:

{{ config(
    tags=["qyra"]
) }}

select ...

Then set Tables configuration to Show models with any of these tags and add the tag:

Selecting models by dbt tag in Tables configuration

Table configuration

You can customize how Tables look in Qyra by adding configuration to your YAML file. Here's an example of most the properties you can use when defining a Table:

models:
  - name: users
    config:
      meta:
        label: 'App Users'
        order_fields_by: 'label'
        groups: ['mobile', 'engagement']
        sql_filter: ${date_dimension} >= '2025-01-01'
        primary_key: user_id
        sets:
    	    event_fields:
      	  fields:
              - user_id
              - event_type
        joins:
          - join: events
            sql_on: ${users.user_id} = ${events.user_id}
            fields: [event_fields*]
            relationship: one-to-many
        required_attributes:
          product_team: 'Mobile'
        explores:
          users_pii:
            required_attributes:
              has_pii_access: true
            joins:
              - join: users_pii
                sql_on: ${users.user_id} = ${users_pii.user_id}
                relationship: one-to-one
models:
  - name: users
    meta:
      label: 'App Users'
      order_fields_by: 'label'
      groups: ['mobile', 'engagement']
      sql_filter: ${date_dimension} >= '2025-01-01'
      primary_key: user_id
      sets:
  	    event_fields:
    	  fields:
            - user_id
            - event_type
      joins:
        - join: events
          sql_on: ${users.user_id} = ${events.user_id}
          fields: [event_fields*]
          relationship: one-to-many
      required_attributes:
        product_team: 'Mobile'
      explores:
        users_pii:
          required_attributes:
            has_pii_access: true
          joins:
            - join: users_pii
              sql_on: ${users.user_id} = ${users_pii.user_id}
              relationship: one-to-one
type: model
name: users

label: 'App Users'
order_fields_by: 'label'
groups: ['mobile', 'engagement']
sql_filter: ${date_dimension} >= '2025-01-01'
primary_key: user_id
sets:
  event_fields:
    fields:
      - user_id
      - event_type
joins:
  - join: events
    sql_on: ${users.user_id} = ${events.user_id}
    fields: [event_fields*]
    relationship: one-to-many
required_attributes:
  product_team: 'Mobile'
explores:
  users_pii:
    required_attributes:
      has_pii_access: true
    joins:
      - join: users_pii
        sql_on: ${users.user_id} = ${users_pii.user_id}
        relationship: one-to-one

Table properties

PropertyValueNote
labelstringCustom label. This is what you'll see in Qyra instead of the Table name.
order_fields_byindex or labelHow the fields will be sorted in the sidebar. Read about the order rules.
joinsarrayJoin logic to join other data models to the Table. Read about joins.
metricsobjectModel metrics. Read about model metrics
groupsarray of stringsGroup tables in the sidebar, with up to 5 levels of nesting. Read about groups.
group_labelstringDeprecated Use groups instead. Group tables in the sidebar.
sql_fromstringOverrides dbt model relation_name
sql_filterstringA permanent filter that will always be applied when querying this table directly. Read about sql_filter.
sql_wherestringAlias for sql_filter
required_attributesobjectLimits access to users with those attributes (AND logic). Read about user attributes
any_attributesobjectLimits access to users with those attributes (OR logic). Read about user attributes
group_detailsobjectDescribes the groups for dimensions and metrics
default_filtersarrayDimension filters that will be applied when no other filter on those dimension exists. Read about default_filters
default_show_underlying_valuesarrayDefault fields shown in "View underlying data" for all metrics in this model. Read about default_show_underlying_values
exploresobjectAllows you to define multiple table explores in Qyra from a single dbt model.
hiddenbooleanIf true, the model's own explore is not created. The table can still be joined to and used as the base for explores. Read about hiding the base explore.
parametersobjectModel-level parameters that can be referenced in SQL properties. Read about parameters
setsobjectAllows you to define a reference to a collection of fields. This reference can be re-used throughout the model.
case_sensitivebooleanIf set to false, string filters on dimensions in this table will be case insensitive by default. Defaults to true.

Adding a new dbt model

If you've added a new dbt model to your project, you need to do dbt run + dbt refresh before it will appear in Qyra.

Qyra gets information about your data models from dbt. But it gets information about the data generated by those data models from your data warehouse.

This means that if you add a new dbt model to your project or update a model so that you're making changes to the table it generates, then you need to do two things before your changes will appear in Qyra:

  1. Materialize the new table or changes using dbt run. You want the data in your data warehouse to be the new table you're expecting. So you need to do dbt run to update the table from the data model you just changed.
  2. Click Refresh dbt in Qyra or run qyra refresh in the CLI. This will re-sync your dbt project in Qyra so that changes you made to your dbt models are shown in Qyra (e.g. adding a new table or column).

Order fields by

By default, the fields in your sidebar for any table will appear alphabetically (order_fields_by: "label"). Sometimes, you might not want your fields to appear alphabetically, but instead, in the same order as they are in your YAML file. You can achieve this by setting the order_fields_by parameter in your table's meta tag to index, like this:

models:
  - name: users
    config:
      meta:
        order_fields_by: 'index'
    columns:
      - name: user_id
      - name: user_name
      - name: user_email
models:
  - name: users
    meta:
      order_fields_by: 'index'
    columns:
      - name: user_id
      - name: user_name
      - name: user_email
type: model
name: users

order_fields_by: 'index'

dimensions:
  - name: user_id
  - name: user_name
  - name: user_email

So, in the example above, the fields in the sidebar for "My Table" would appear in the order:

  • user_id
  • user_name
  • user_email

Instead of being listed alphabetically.

Here are some other things worth mentioning about the order_fields_by parameter:

  • By default, order_fields_by is set to label, which means that your fields will appear in the table listed alphabetically.
  • Since metrics can be declared in multiple places within your YAML (as a dbt metric, in the model meta tag, under a dimension's meta), we force the following order on metrics if you set order_fields_by to index:
    • dbt metrics appear first
    • then, metrics defined in the model's meta
    • then, metrics defined in the dimensions' meta
  • Group labels inherit the index of the first dimension that use them.

Groups

Use groups to organize tables into nested groups in the sidebar. You can nest up to 5 levels deep by passing an array of group keys, ordered from outermost to innermost.

models:
  - name: users
    meta:
      groups: ['mobile', 'engagement']

In the example above, the users table is placed under the mobile group, and within it, the engagement subgroup.

Group keys are referenced project-wide. Define their display labels and (optional) descriptions in your qyra.config.yml file under table_groups:

# qyra.config.yml
table_groups:
  mobile:
    label: 'Mobile App'
    description: 'Tables related to the mobile app'
  engagement:
    label: 'Engagement'

If a group key is not defined in table_groups, the key itself is used as the label.

The tables in your sidebar will appear in the following order:

  • Grouped tables appear first, with groups sorted alphabetically by label
  • Ungrouped tables appear after the grouped tables, alphabetically
  • Tables within each group are also ordered alphabetically

The groups property replaces the legacy group_label property. group_label still works for backwards compatibility but is deprecated — migrate to groups for nested group support.

Tables sidebar where a Staging group expands to Stg customers, Stg orders, and Stg payments above ungrouped tables

SQL from

sql_from is a configuration option that overrides the default dbt model relation name when generating SQL queries in Qyra.

For example, you might use this if you want your Qyra explore to query from a specific materialized view, a different schema, or include additional SQL logic in the FROM clause while still maintaining the dbt model structure for dimensions and metrics.

models:
  - name: sales
    config:
      meta:
        sql_from: my_schema.my_sales_view
models:
  - name: sales
    meta:
      sql_from: my_schema.my_sales_view
type: model
name: sales

sql_from: my_schema.my_sales_view

SQL filter (row-level security)

sql_filter adds a filter to the table that cannot be removed in Qyra. It is automatically added to the compiled SQL when running queries.

For example:

models:
  - name: sales
    config:
      meta:
        sql_filter: ${TABLE}.sales_region = 'EMEA'
models:
  - name: sales
    meta:
      sql_filter: ${TABLE}.sales_region = 'EMEA'
type: model
name: sales

sql_filter: ${TABLE}.sales_region = 'EMEA'

Any queries that I run using the Sales table in Qyra will always have a filter for sales_region = 'EMEA' in their compiled SQL

select [...]
from qyra.prod.sales
where sales_region = 'EMEA'
Row-level security using user attributes

Using sql_filter with user attributes allows you to set up row-level security in your tables. You can reference user attributes in your sql_filter using ${qyra.attributes.my_attribute_name}

For example:

models:
  - name: sales
    config:
      meta:
        sql_filter: ${TABLE}.sales_region IN (${qyra.attributes.sales_region})
models:
  - name: sales
    meta:
      sql_filter: ${TABLE}.sales_region IN (${qyra.attributes.sales_region})
type: model
name: sales

sql_filter: ${TABLE}.sales_region IN (${qyra.attributes.sales_region})
sql_filter will only be applied when querying tables directly.

For example:

  • Table A is joined to Table B
  • Table B has a sql_filter applied to it
  • A user queries Table A and adds a field from the joined table (Table B) to their query
  • the sql_filter from Table B will not be applied to the query (you would need to add this as a sql_filter to Table A directly for it to apply)
If you reference a dimension from a joined table in your sql_filter, the referenced table will always be joined in your queries.

For example:

  • You have Table A which is joined to Table B
  • In Table A, you've added a sql_filter: ${TABLE}.sales_region = 'EMEA' OR ${table_b}.sales_region IS NULL
  • Table B will always be joined to Table A in your queries (even if there are no fields from Table B selected in your results table)

Required attributes

Qyra can use user attributes to limit some tables to some users.

In the example below, only users with is_admin attribute true can use the payments table. Users without access to this table will not see it on the tables page or the explore page when joined to other tables.

models:
  - name: payments
    config:
      meta:
        required_attributes:
          is_admin: "true"
models:
  - name: payments
    meta:
      required_attributes:
        is_admin: "true"
type: model
name: payments

required_attributes:
  is_admin: "true"

If a user without access to this table runs a query that contains this table, 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.

models:
  - name: payments
    config:
      meta:
        any_attributes:
          department: ["sales", "finance"]
          role: "analyst"
models:
  - name: payments
    meta:
      any_attributes:
        department: ["sales", "finance"]
        role: "analyst"
type: model
name: payments

any_attributes:
  department: ["sales", "finance"]
  role: "analyst"

You can combine required_attributes and any_attributes on the same table. When both are set, both checks must pass. See user attributes for details.

Defining primary keys

You can specify a primary key for your model to uniquely identify each row. This is important for tables as it helps Qyra understand the relationships between tables and prevent data duplication, especially when dealing with SQL fanouts in joins.

The primary key can be defined in two ways:

Single column primary key

If your table has a single column that uniquely identifies each row, you can define it as a string:

models:
  - name: users
    config:
      meta:
        primary_key: user_id
models:
  - name: users
    meta:
      primary_key: user_id
type: model
name: users

primary_key: user_id

Complex primary key

If your table requires multiple columns to uniquely identify each row, you can define the primary key as an array of strings:

models:
  - name: order_items
    config:
      meta:
        primary_key: [order_id, item_id]
models:
  - name: order_items
    meta:
      primary_key: [order_id, item_id]
type: model
name: order_items

primary_key: [order_id, item_id]

Using a properly defined primary key helps Qyra optimize queries and provide accurate results when working with joined tables. It's especially important for preventing metric inflation in SQL joins where duplicate rows can lead to incorrect aggregations.

Default show underlying values

You can set a default show_underlying_values configuration at the model level that applies to all metrics in that model. This is useful when you want consistent underlying data visibility across all metrics without repeating the same configuration for each metric.

Individual metrics can override the model-level default by specifying their own show_underlying_values.

models:
  - name: payments
    config:
      meta:
        default_show_underlying_values:
          - orders.customer_id
          - unique_payment_count
        metrics:
          total_revenue:
            type: sum
            sql: ${TABLE}.revenue
            # Uses the model default: orders.customer_id, unique_payment_count
          average_revenue:
            type: average
            sql: ${TABLE}.revenue
            show_underlying_values:
              - custom_field  # Overrides the model default
models:
  - name: payments
    meta:
      default_show_underlying_values:
        - orders.customer_id
        - unique_payment_count
      metrics:
        total_revenue:
          type: sum
          sql: ${TABLE}.revenue
          # Uses the model default: orders.customer_id, unique_payment_count
        average_revenue:
          type: average
          sql: ${TABLE}.revenue
          show_underlying_values:
            - custom_field  # Overrides the model default
type: model
name: payments

default_show_underlying_values:
  - orders.customer_id
  - unique_payment_count

metrics:
  total_revenue:
    type: sum
    sql: ${TABLE}.revenue
    # Uses the model default: orders.customer_id, unique_payment_count
  average_revenue:
    type: average
    sql: ${TABLE}.revenue
    show_underlying_values:
      - custom_field  # Overrides the model default

The list of fields must be dimension names (or metrics if you'd like to include them) from the base table or from any joined tables. To reference a field from a joined table, prefix the field name with the joined table name, like this: my_joined_table_name.my_dimension.

The order that the fields are listed in default_show_underlying_values is the order that they'll appear in on the "View underlying data" table.

Default filters

Use default_filters to define filters on Dimensions that will be applied when no other user-defined filter on those Dimensions exists. Default filters will apply to tables on load and can be populated with a pre-determined value. Use them to suggest to users the kind of filters they might want to consider, or provide a default filtered view of a table that can be changed if needed.

An optional required flag can be added, and if this flag is set to required: true, the filter for this field cannot be removed from the UI but users can still modify the operator and value to adjust how this filter is applied to the data. This ensures the filter is always applied in some form, while giving users flexibility to change the date range or other values. This can be particularly useful if you have a large table and want to force users to filter on a partitioned date.

In contrast, when required: false (or omitted), the filter is pre-populated but fully editable — users can change the field, operator, value, or remove it entirely.

default_filters and required_filters can only target dimensions (including visible additional_dimensions). Hidden dimensions (hidden: true) are not valid targets and will fail compilation.

Below you can see there is a default filter with the optional required flag, that will have show the last 14 days of data by default.

models:
  - name: orders
    config:
      meta:
        default_filters:
          - date: 'inThePast 14 days'
		  required: true
    columns:
      - name: date
        description: 'Order date'
        config:
          meta:
            dimension:
              type: date
models:
  - name: orders
    meta:
      default_filters:
        - date: 'inThePast 14 days'
		  required: true
    columns:
      - name: date
        description: 'Order date'
        meta:
          dimension:
            type: date
type: model
name: orders

default_filters:
  - date: 'inThePast 14 days'
    required: true

dimensions:
  - name: date
    description: 'Order date'
    type: date

Note: Required filters are not a security feature. For secure data access control, use user attributes.

If you have many filters in your list, they will be joined using AND

  - name: orders
    config:
      meta:
        default_filters:
          - date: 'inThePast 14 days'
          - status: "completed"
    columns:
      - name: date
        description: 'Order date'
        config:
          meta:
            dimension:
              type: date
      - name: status
        description: 'Order status - completed, pending, cancelled'
        config:
          meta:
            dimension:
              type: string
  - name: orders
    meta:
      default_filters:
        - date: 'inThePast 14 days'
        - status: "completed"
    columns:
      - name: date
        description: 'Order date'
        meta:
          dimension:
            type: date
      - name: status
        description: 'Order status - completed, pending, cancelled'
        meta:
          dimension:
            type: string
type: model
name: orders

default_filters:
  - date: 'inThePast 14 days'
  - status: "completed"

dimensions:
  - name: date
    description: 'Order date'
    type: date
  - name: status
    description: 'Order status - completed, pending, cancelled'
    type: string

In the example above, the orders table will have a default filter of date in the past 14 days and status completed. Both can be removed by the user, as the required flag is not present.

Note that we do also support a legacy structure for defining required filters, see below:

models:
  - name: orders
    config:
      meta:
        required_filters:
          - date: 'inThePast 14 days'
    columns:
      - name: date
        description: 'Order date'
        config:
          meta:
            dimension:
              type: date
models:
  - name: orders
    meta:
      required_filters:
        - date: 'inThePast 14 days'
    columns:
      - name: date
        description: 'Order date'
        meta:
          dimension:
            type: date
type: model
name: orders

required_filters:
  - date: 'inThePast 14 days'

dimensions:
  - name: date
    description: 'Order date'
    type: date

Available filter types

OperatorDescriptionExample (as code)Supported Types
isValue is equal touser_name: "katie"String, Number, Date
is notValue is not equal touser_name: "!katie"String, Number, Date
betweenValue is between two values (inclusive)age: "between 18 and 65"Number, Date
order_date: "between 2024-01-01 and 2024-12-31"Number, Date
containsString contains valueuser_name: "%katie%"String
does not containString does not contain valueuser_name: "!%katie%"String
starts withString starts with valueuser_name: "katie%"String
ends withString ends with valueuser_name: "%katie"String
is greater thanNumber is greater than valuenum_orders: "> 4"Number
is greater than or equal toNumber is greater than or equal to valuenum_orders: ">= 4"Number
is less thanNumber is less than valuenum_orders: "< 4"Number
is less than or equal toNumber is less than or equal to valuenum_orders: "<= 4"Number
in the pastDate is before x (days / months / years)date: "inThePast 14 days"Date
in the nextDate is after x (days / months / years)date: "inTheNext 30 days"Date
in the currentDate is in the current (day / week / month / quarter / year)date: "inTheCurrent day"Date
not in the currentDate is not in the current (day / week / month / quarter / year)date: "notInTheCurrent week"Date
is nullValue is NULLstatus: "null"All
is not nullValue is not NULLstatus: "!null"All
is emptyValue is empty stringnotes: "empty"String
is not emptyValue is not empty stringnotes: "!empty"String
is [boolean]Boolean value is trueis_complete: "true"Boolean
is not [boolean]Boolean value is false or nullis_complete: "!true"Boolean
in listValue is in a list of valuesstatus: ["active", "pending", "approved"]String, Number

Filter examples

Between operator (numeric):

default_filters:
  - age: 'between 18 and 65'
  - price: 'between 0.5 and 99.9'
  - temperature: 'between -10 and 10'

Between operator (dates):

default_filters:
  - order_date: 'between 2024-01-01 and 2024-12-31'
  - created_at: 'between 2024-01-01T00:00:00 and 2024-12-31T23:59:59'

Multiple values (list):

Use an array to filter by multiple values. This generates a SQL IN clause, e.g. WHERE status IN ('active', 'pending', 'approved').

default_filters:
  - status:
      - 'active'
      - 'pending'
      - 'approved'
  - country:
      - 'US'
      - 'CA'
      - 'UK'

Pattern matching:

default_filters:
  - product_name: '%shirt%'        # Contains "shirt"
  - email: 'admin%'                # Starts with "admin"
  - domain: '%@gmail.com'          # Ends with "@gmail.com"
  - product_name: '!%test%'        # Does not contain "test"

Date intervals:

default_filters:
  - created_date: 'inThePast 7 days'
  - expiry_date: 'inTheNext 30 days'
  - signup_date: 'inThePast 1 years'

Available intervals: milliseconds, seconds, minutes, hours, days, weeks, months, years

Current time period (no value needed):

Use inTheCurrent / notInTheCurrent to filter dynamically to the current time period. These operators take no number — just a unit of time (singular or plural).

default_filters:
  - order_date: 'inTheCurrent day'
  - order_date: 'notInTheCurrent month'

Available units: day, week, month, quarter, year

Complex example with multiple filters:

models:
  - name: orders
    config:
      meta:
        default_filters:
          - status:
              - 'completed'
              - 'shipped'
          - order_date: 'between 2024-01-01 and 2024-12-31'
          - revenue: '>= 100'
          - country: '!test'
          - customer_email: '!%@test.com%'
models:
  - name: orders
    meta:
      default_filters:
        - status:
            - 'completed'
            - 'shipped'
        - order_date: 'between 2024-01-01 and 2024-12-31'
        - revenue: '>= 100'
        - country: '!test'
        - customer_email: '!%@test.com%'
type: model
name: orders

default_filters:
  - status:
      - 'completed'
      - 'shipped'
  - order_date: 'between 2024-01-01 and 2024-12-31'
  - revenue: '>= 100'
  - country: '!test'
  - customer_email: '!%@test.com%'

Filter notes

  • Case insensitivity: Keywords like between, AND, null, empty are case-insensitive
  • Quotes: Use quotes for strings with spaces or special characters
  • Escaping: Use ^ to escape special characters (%, _, ,, !, ^)
  • AND logic: Multiple filters are automatically joined with AND
  • Priority: Default filters only apply when no user-specified filter exists for that dimension
  • Supported fields: default_filters and required_filters apply to dimensions only (not metrics)
  • Hidden fields: Hidden dimensions (hidden: true) cannot be used in default_filters/required_filters

Case sensitive

You can control whether string filters are case sensitive or case insensitive at the table level. By default, all string filters are case sensitive (case_sensitive: true). When set to false, string filters on all dimensions in this table will ignore case differences when matching values.

This setting affects the following string filter operators: equals, not equals, starts with, ends with, includes, and doesn't include.

models:
  - name: customers
    config:
      meta:
        case_sensitive: false
models:
  - name: customers
    meta:
      case_sensitive: false
type: model
name: customers

case_sensitive: false

With case_sensitive: false, filtering for "john" on any string dimension would match "John", "JOHN", "john", etc.

Table-level case_sensitive settings override the project-level default. Dimension-level settings override both. See Dimensions reference for dimension-level configuration and qyra.config.yml reference for project-level defaults.

Parameters configuration

Model-level parameters are defined under the model's meta.parameters key (in Qyra YAML, the top-level parameters key). They are scoped to the model where you define them and use the same property schema as project-wide parameters — reference them in the model's SQL properties with the model name included, ${qyra.parameters.model_name.parameter_name} (or the shorter ${ld.parameters.model_name.parameter_name}).

See the Parameters reference for the full property schema, and the Parameters guide for syntax and examples, including referencing model-level parameters from joined tables.

Explores

You can define multiple table explores from a single table using the explores config. This will allow you to list the same dbt model multiple times in the list of Tables in Qyra. You can use it to show different versions of a table, join different tables to the base table, customize table visibility, etc.

Below is an advanced example of using Explores. This will result in three total tables using the deals model at the base.

  • Deals will not have any joins or limitations
  • Deals w/Accounts will join to the accounts table and show all Accounts fields, but only people with the is_exec user attribute can see it
  • Deals w/Accounts (no Names) will join to the accounts table and only show Industry and Segment dimensions, it has no access restrictions
models:
- name: deals
  config:
    meta:
      primary_key: deal_id
      explores:
        deals_accounts:
          required_attributes:
            is_exec: "true"
          label: 'Deals w/Accounts'
          description: The deals table with the Accounts table details included
          joins:
          - join: accounts
            relationship: many-to-one
            sql_on: ${deals.account_id} = ${accounts.account_id}
        deals_accounts_no_names:
          label: 'Deals w/Accounts (no Names)'
          description: The deals table with the Accounts table details included
          joins:
          - join: accounts
            relationship: many-to-one
            sql_on: ${deals.account_id} = ${accounts.account_id}
            fields: [industry, segment, unique_accounts, unique_smb_accounts, unique_midmarket_accounts, unique_enterprise_accounts]
models:
- name: deals
  meta:
    primary_key: deal_id
    explores:
      deals_accounts:
        required_attributes:
          is_exec: "true"
        label: 'Deals w/Accounts'
        description: The deals table with the Accounts table details included
        joins:
        - join: accounts
          relationship: many-to-one
          sql_on: ${deals.account_id} = ${accounts.account_id}
      deals_accounts_no_names:
        label: 'Deals w/Accounts (no Names)'
        description: The deals table with the Accounts table details included
        joins:
        - join: accounts
          relationship: many-to-one
          sql_on: ${deals.account_id} = ${accounts.account_id}
          fields: [industry, segment, unique_accounts, unique_smb_accounts, unique_midmarket_accounts, unique_enterprise_accounts]
type: model
name: deals

primary_key: deal_id
explores:
  deals_accounts:
    required_attributes:
      is_exec: "true"
    label: 'Deals w/Accounts'
    description: The deals table with the Accounts table details included
    joins:
    - join: accounts
      relationship: many-to-one
      sql_on: ${deals.account_id} = ${accounts.account_id}
  deals_accounts_no_names:
    label: 'Deals w/Accounts (no Names)'
    description: The deals table with the Accounts table details included
    joins:
    - join: accounts
      relationship: many-to-one
      sql_on: ${deals.account_id} = ${accounts.account_id}
      fields: [industry, segment, unique_accounts, unique_smb_accounts, unique_midmarket_accounts, unique_enterprise_accounts]

All the table configuration options can be used under the explores tag.

Read this guide to learn more about explores

Hiding the base explore

By default, a model with explores still gets an explore of its own, so the model appears in the Tables list alongside its curated explores. If you only want to expose the curated explores, set hidden: true on the model. This is useful when you want to scope access to a curated explore only — for example, giving an AI agent access to a curated explore via explore-level tags without also exposing the underlying model.

hidden only skips the model's own explore. The model is still compiled as a table, so it remains available as a join target from other tables and as the base table for its explores entries.

models:
- name: deals
  config:
    meta:
      hidden: true
      primary_key: deal_id
      explores:
        deals_curated:
          label: 'Deals (Curated)'
          description: The curated view of the deals table
          tags: ['ai']
models:
- name: deals
  meta:
    hidden: true
    primary_key: deal_id
    explores:
      deals_curated:
        label: 'Deals (Curated)'
        description: The curated view of the deals table
        tags: ['ai']
type: model
name: deals

hidden: true
primary_key: deal_id
explores:
  deals_curated:
    label: 'Deals (Curated)'
    description: The curated view of the deals table
    tags: ['ai']

In this example, only Deals (Curated) appears in Qyra — the base Deals explore is not created.

Sets

Sometimes you may find that you're redeclaring the same set of fields for things like joins and show_underlying_values. In this case, you can define a set. A set allows you to associate those fields to a single value. That reference can then be used any place you would normally define fields.

# Define a set
sets:
  my_set:
    fields:
      - user_id
      - user_name
      - created_at

# Referencing the set
fields: [my_set*]

# Qyra resolves to
fields: [user_id, user_name, created_at]

Expand

The expand operator (ex. my_set*) tells Qyra to look up the set being referenced and resolve it to the associated collection of fields. When Qyra compiles your model, it will replace set reference names with the actual fields.

models:
  - name: orders_model
    config:
      meta:
        sets:
          my_user_fields:
            fields:
              - user_id
              - user_name
    columns:
      - name: revenue
        config:
          meta:
            metrics:
              sum_revenue:
                type: sum
                show_underlying_values:
                  - my_user_fields* # Reference to the set we defined
models:
  - name: orders_model
    meta:
      sets:
        my_user_fields:
          fields:
            - user_id
            - user_name
    columns:
      - name: revenue
        meta:
          metrics:
            sum_revenue:
              type: sum
              show_underlying_values:
                - my_user_fields* # Reference to the set we defined
type: model
name: orders_model

sets:
  my_user_fields:
    fields:
      - user_id
      - user_name

dimensions:
  - name: revenue

metrics:
  sum_revenue:
    type: sum
    sql: ${TABLE}.revenue
    show_underlying_values:
      - my_user_fields* # Reference to the set we defined

Exclusions

In the instance where you want to leverage some, but not all fields in a set, you can use the excludes operator (ex. -field_name). The exclusion needs to be used in conjunction with an expand operator. This tells Qyra to expand a given set while omitting any field name using the exclusion operator.

models:
  - name: orders_model
    config:
      meta:
        sets:
          my_user_fields:
            fields:
              - user_id
              - user_name
    columns:
      - name: revenue
        config:
          meta:
            metrics:
              sum_revenue:
                type: sum
                show_underlying_values:
                  - my_user_fields*
                  - -user_id # Expand `my_user_fields` and exclude `user_id`
models:
  - name: orders_model
    meta:
      sets:
        my_user_fields:
          fields:
            - user_id
            - user_name
    columns:
      - name: revenue
        meta:
          metrics:
            sum_revenue:
              type: sum
              show_underlying_values:
                - my_user_fields*
                - -user_id # Expand `my_user_fields` and exclude `user_id`
type: model
name: orders_model

sets:
  my_user_fields:
    fields:
      - user_id
      - user_name

dimensions:
  - name: revenue

metrics:
  sum_revenue:
    type: sum
    sql: ${TABLE}.revenue
    show_underlying_values:
      - my_user_fields*
      - -user_id # Expand `my_user_fields` and exclude `user_id`

Joins

Within a model, you may join with other model tables. The sets you define can reference those joined fields as well! Use dot notation to reference a joined table:

models:
  - name: purchases
    columns: <...>
    config:
      meta:
        sets:
    	    revenue_fields:
      	  fields:
              - purchase_date
              - purchase_amount
              - user.user_name # Referencing the joined table
        joins:
          - join: user
            sql_on: ${users.user_id} = ${purchases.user_id}
            relationship: one-to-many
        metrics:
          revenue:
            type: sum
            show_underlying_values:
              - revenue_fields*
models:
  - name: purchases
    columns: <...>
    meta:
      sets:
  	    revenue_fields:
    	  fields:
            - purchase_date
            - purchase_amount
            - user.user_name # Referencing the joined table
      joins:
        - join: user
          sql_on: ${users.user_id} = ${purchases.user_id}
          relationship: one-to-many
      metrics:
        revenue:
          type: sum
          show_underlying_values:
            - revenue_fields*
type: model
name: purchases

sets:
  revenue_fields:
    fields:
      - purchase_date
      - purchase_amount
      - user.user_name # Referencing the joined table

joins:
  - join: user
    sql_on: ${users.user_id} = ${purchases.user_id}
    relationship: one-to-many

metrics:
  revenue:
    type: sum
    sql: ${TABLE}.revenue
    show_underlying_values:
      - revenue_fields*

Remember, in order to include fields from another table in your set, you need ensure that you have joined the table the set originates from before defining the set.