## Contact
---
#### Contact us
---
sidebar_class_name: hidden
title: Contact us
hide_table_of_contents: true
---
Rill has a globally distributed team and we'd love to hear from you—whether you're a customer, prospect, or simply a Rill enthusiast. There are several ways to get in touch with the Rill team and engage with the community, which are described below.
## GitHub
Rill is part of the open source community, and you can find the source code of our Rill Developer project on GitHub [here](https://github.com/rilldata/rill). We welcome contributions, but if you simply want to report a bug, file an issue, or request a feature, you can do so on [this page](https://github.com/rilldata/rill/issues).
## Discord
Curious to see if someone else has encountered an issue before, looking for help getting started, interested in tapping into the collective knowledge of others, or simply want to engage with other members of the Rill community? Rill's community can be found on Discord, and we'd love for you to be a part of it! Please click [here](https://discord.gg/2ubRfjC7Rh) to access our dedicated Discord channel.
## Contacting Support
If you'd like to get in contact with the Rill team directly and/or contact Rill Support, there are a few different options available.
### Slack channel
If you're an active prospect or customer, you will have access to a dedicated Slack channel with the Rill team. This ensures we can provide you with individualized responses to your questions or issues, with the fastest response times from our distributed team.
:::info Interested in evaluating Rill Cloud?
Please let us know and we'd be happy to set up a Slack channel for your team!
:::
### Email
For individuals who prefer a more traditional medium, you can open a support ticket by reaching out to support@rilldata.com.
### In-app chat
For logged-in users, Rill provides the ability to ask a question or report an issue to the Rill Support team directly from within the application via an embedded in-app chat widget. Select your user icon in the top right corner and choose "Contact Rill support"—a chat widget will appear on your screen!

---
## Developers
---
### Build
---
#### Connectors
---
##### Data Source
---
####### Data Sources
---
title: Data Sources
sidebar_label: Data Sources
sidebar_position: 0
toc_max_heading_level: 3
className: connect-connect
---
By default, Rill will use a managed embedded analytics engine (**DuckDB** or **ClickHouse**) to support data ingestion. Whether you're working with cloud data warehouses, databases, file storage, or streaming data sources, Rill provides seamless connectivity and data ingestion capabilities. Once you have connected to your data source you can create [downstream models](/developers/build/models), [metrics views](/developers/build/metrics-view) and [visualize your data](/developers/build/dashboards).
:::tip using clickhouse?
Don't forget to [create a managed ClickHouse server](/developers/build/connectors/olap/clickhouse#rill-managed-clickhouse) before getting started!
```yaml
type: connector
driver: clickhouse
managed: true
```
:::
import ConnectorIcon from '@site/src/components/ConnectorIcon';
In order to connect and browse through your data, you'll need to create a connector file. Browse through the options below for our supported connectors. Each connector is designed to handle the specific authentication and configuration requirements of your data source.
:::warning OLAP Engine Limitations
Rill supports connecting your data to both [DuckDB](/developers/build/connectors/olap/duckdb) and [ClickHouse](/developers/build/connectors/olap/clickhouse). However, there are still some features in development for managed ClickHouse. For more information see our [managed ClickHouse docs](/developers/build/connectors/olap/clickhouse#rill-managed-clickhouse). If you've still got questions, [contact our team](/contact) for more information and scheduled feature releases!
:::
## Data Warehouses
### Athena
### BigQuery
### Databricks
### Redshift
### Snowflake
}
header="Athena"
content="Connect to Amazon Athena for serverless querying of data stored in S3 using standard SQL."
link="/developers/build/connectors/data-source/athena"
linkLabel="Learn more"
referenceLink="athena"
/>
}
header="BigQuery"
content="Connect to Google BigQuery for analytics and data warehousing with service account authentication."
link="/developers/build/connectors/data-source/bigquery"
linkLabel="Learn more"
referenceLink="bigquery"
/>
}
header="Databricks"
content="Connect to Databricks SQL warehouses with personal access token authentication."
link="/developers/build/connectors/data-source/databricks"
linkLabel="Learn more"
referenceLink="databricks"
/>
}
header="Redshift"
content="Connect to Amazon Redshift data warehouse with AWS credentials and support for both provisioned and serverless clusters."
link="/developers/build/connectors/data-source/redshift"
linkLabel="Learn more"
referenceLink="redshift"
/>
}
header="Snowflake"
content="Connect to Snowflake data warehouse with support for individual credentials and JWT authentication."
link="/developers/build/connectors/data-source/snowflake"
linkLabel="Learn more"
referenceLink="snowflake"
/>
## Databases
### MySQL
### PostgreSQL
### SQLite
### Supabase
}
header="MySQL"
content="Connect to MySQL databases with support for various authentication methods and SSL connections."
link="/developers/build/connectors/data-source/mysql"
linkLabel="Learn more"
referenceLink="mysql"
/>
}
header="PostgreSQL"
content="Connect to PostgreSQL databases with support for SSL connections and various authentication methods."
link="/developers/build/connectors/data-source/postgres"
linkLabel="Learn more"
referenceLink="postgresql"
/>
}
header="SQLite"
content="Connect to SQLite databases for lightweight, file-based data storage and querying."
link="/developers/build/connectors/data-source/sqlite"
linkLabel="Learn more"
/>
}
header="Supabase"
content="Connect to Supabase's managed PostgreSQL databases with SSL support and standard connection methods."
link="/developers/build/connectors/data-source/supabase"
linkLabel="Learn more"
referenceLink="supabase"
/>
## Object Storage
### Amazon S3
### Google Cloud Storage
### Microsoft Azure Blob Storage
}
header="Amazon S3"
content="Connect to Amazon S3 buckets to read data files including CSV, JSON, Parquet, and compressed formats."
link="/developers/build/connectors/data-source/s3"
linkLabel="Learn more"
referenceLink="s3"
/>
}
header="Google Cloud Storage"
content="Google Cloud Storage for scalable object storage and data lakes."
link="/developers/build/connectors/data-source/gcs"
linkLabel="Learn more"
referenceLink="gcs"
/>
}
header="Azure"
content="Connect to Microsoft Azure Blob Storage to read data files with support for various formats."
link="/developers/build/connectors/data-source/azure"
linkLabel="Learn more"
referenceLink="azure"
/>
## Table Formats
### Delta Lake
### Apache Iceberg
}
header="Delta Lake"
content="Read Delta tables directly from object storage through compatible query engines."
link="/developers/build/connectors/data-source/delta"
linkLabel="Learn more"
/>
}
header="Apache Iceberg"
content="Read Iceberg tables directly from object storage through compatible query engines."
link="/developers/build/connectors/data-source/iceberg"
linkLabel="Learn more"
/>
## Other Data Connectors
### External DuckDB
### Google Sheets
### HTTPS
### Local File
### Salesforce
}
header="DuckDB"
content="Attach your local DuckDB database to Rill's embedded database."
link="/developers/build/connectors/data-source/duckdb"
linkLabel="Learn more"
referenceLink="external-duckdb"
/>
}
header="Google Sheets"
content="Connect to Google Sheets to read data from spreadsheets."
link="/developers/build/connectors/data-source/googlesheets"
linkLabel="Learn more"
/>
https:// }
header="HTTPS"
content="Download data from HTTP/HTTPS URLs with support for various authentication methods."
link="/developers/build/connectors/data-source/https"
linkLabel="Learn more"
referenceLink="https"
/>
}
header="Local File"
content="Read data from local files including CSV, JSON, Parquet, and compressed formats."
link="/developers/build/connectors/data-source/local-file"
linkLabel="Learn more"
/>
}
header="Salesforce"
content="Connect to Salesforce to extract data from objects and queries using the Salesforce API."
link="/developers/build/connectors/data-source/salesforce"
linkLabel="Learn more"
/>
## Externally Hosted Services
If you have a firewall in front of your externally hosted service, you will need to whitelist the IP addresses below. This will allow you to connect to/from your service once your project is deployed to Rill Cloud.
```
35.196.245.100
34.74.117.37
35.196.153.31
34.75.22.143
34.148.167.51
35.237.60.193
```
## Managed OLAP Engine Caveats
When deciding on which managed OLAP engine to use with Rill, you'll need to decide based on the following factors:
- **Size of data**: Consider the volume and growth rate of your datasets
- **Familiarity with respective OLAP engine features**: Assess your team's expertise with each engine's capabilities
- **Integration complexity**: Consider how well each engine integrates with your existing data infrastructure
In the case of **sub 100GB of data**, we recommend keeping the default engine, DuckDB, in order to minimize the integration complexity. The reason for this is that [DuckDB has built-in functions](https://duckdb.org/docs/stable/data/data_sources) to support the connectors listed on this page.
On the other hand, if you need to analyze **100s of GB of data**, we would recommend using Managed ClickHouse. This will add some complexity ([staging tables](/developers/build/models/staging-models)), but will in turn provide better dashboard performance.
If data leans either way, a good deciding factor for which OLAP engine to use is your **familiarity with their SQL syntax**. Whether you're [creating models](/developers/build/models/models-101#intermediate-processing) or using [arithmetic functions](/developers/build/metrics-view) in the metrics view, you'll need to utilize the engine's built-in functions.
:::note Supported Connectors
If there's a connector that you're interested in or you're looking for the list of currently supported ClickHouse connectors, [contact us](/contact)!
:::
---
####### Delta Lake
---
title: Delta Lake
description: Read Delta Lake tables from object storage
sidebar_label: Delta Lake
sidebar_position: 12
---
## Overview
[Delta Lake](https://delta.io/) is an open-source storage framework that brings ACID transactions to data lakes. Rill supports reading Delta tables directly from object storage through compatible query engine integrations. Today, this is powered by DuckDB's [Delta extension](https://duckdb.org/docs/stable/core_extensions/delta).
:::note Direct file access only
Rill reads Delta tables by scanning the table's transaction log and data files directly from object storage. Catalog-based access (e.g., through Unity Catalog) is not currently supported.
:::
## Storage Backends
Delta tables can be read from any of the following storage backends:
| Backend | URI format | Authentication |
|---|---|---|
| Amazon S3 | `s3://bucket/path/to/table` | Requires an [S3 connector](/developers/build/connectors/data-source/s3) |
| Azure Blob Storage | `azure://container/path/to/table` | Requires an [Azure connector](/developers/build/connectors/data-source/azure) |
| Local filesystem | `/path/to/table` | No authentication needed |
:::info GCS not yet supported
Google Cloud Storage is not currently supported for Delta tables. GCS support depends on the upstream DuckDB Delta extension adding it.
:::
For cloud storage backends, you must first configure the corresponding storage connector with valid credentials. Rill uses these credentials to authenticate when reading the Delta table files.
## Using the UI
1. Click **Add Data** in your Rill project
2. Select **Delta Lake** as the data source type
3. Choose your storage backend (S3, Azure, or Local)
4. Enter the path to your Delta table directory
5. Enter a model name and click **Create**
For cloud storage backends, the UI will prompt you to set up the corresponding storage connector if one doesn't already exist.
## Manual Configuration
Create a model that uses DuckDB's `delta_scan()` function to read the table.
### Reading from S3
Create `models/delta_data.yaml`:
```yaml
type: model
connector: duckdb
create_secrets_from_connectors: s3
materialize: true
sql: |
SELECT *
FROM delta_scan('s3://my-bucket/path/to/delta_table')
```
### Reading from Azure
```yaml
type: model
connector: duckdb
create_secrets_from_connectors: azure
materialize: true
sql: |
SELECT *
FROM delta_scan('azure://my-container/path/to/delta_table')
```
### Reading from local filesystem
```yaml
type: model
connector: duckdb
materialize: true
sql: |
SELECT *
FROM delta_scan('/path/to/delta_table')
```
## Deploy to Rill Cloud
Since Delta tables are read through DuckDB using your existing storage connector credentials, deploying to Rill Cloud follows the same process as the underlying storage connector:
- **S3**: Follow the [S3 deployment guide](/developers/build/connectors/data-source/s3#deploy-to-rill-cloud)
- **Azure**: Follow the [Azure deployment guide](/developers/build/connectors/data-source/azure#deploy-to-rill-cloud)
Ensure your storage connector credentials are configured in your Rill Cloud project before deploying.
## Limitations
- **Direct file access only**: Rill reads Delta transaction logs and data files directly from storage. Catalog integrations (e.g., Unity Catalog) are not supported.
- **DuckDB engine**: Delta support is currently provided through DuckDB's Delta extension. Additional engine support is planned.
- **No GCS support**: Google Cloud Storage is not yet supported by the Delta extension.
- **Read-only**: Rill reads from Delta tables but does not write to them.
- **Experimental**: The DuckDB Delta extension is currently marked as experimental.
---
####### External DuckDB
---
title: External DuckDB
description: Connect to external DuckDB databases and ingest data into Rill
sidebar_label: External DuckDB
sidebar_position: 11
---
While not recommended for production use, Rill allows you to `attach` external DuckDB databases to ingest data from them as a data source. This approach has several caveats and limitations during deployment and is primarily intended for local testing scenarios.
:::warning Local Development Only
There are several limitations with deployment to Rill Cloud, so we do not recommend this method for production environments. Key limitations include:
- Moving your DuckDB file into the `data/` folder within your project directory
- A size limitation of 100MB when deploying to Rill Cloud
[Contact us](/contact) if you have questions or encounter issues with these limitations.
:::
## Attaching an External DuckDB
In the default `connectors/duckdb.yaml` file, you can use the `init_sql` parameter to execute SQL statements during database initialization, such as attaching an external database to Rill's embedded DuckDB. For more details on the YAML configuration, see the [DuckDB reference page](/reference/project-files/connectors#duckdb).
```yaml
type: connector
driver: duckdb
managed: true
init_sql:
ATTACH '/path/to/your/duckdb.db' AS external_duckdb;
INSTALL httpfs;
LOAD httpfs;
```
## Importing Data to Your External DuckDB
After establishing a connection, you can import data through the connector UI. This process will write data from your attached database to [Rill's embedded DuckDB.](/developers/build/connectors/olap/duckdb#rill-managed-duckdb)
```yaml
# Model YAML
# Reference documentation: https://docs.rilldata.com/reference/project-files/models
type: model
materialize: true
connector: duckdb
sql: SELECT * from external_duckdb.local_table
```
---
####### Google Sheets
---
title: Google Sheets
description: Connect to data in Google Sheets
sidebar_label: Google Sheets
sidebar_position: 20
---
### Google Sheets
Rill can read from any HTTP(S) URL endpoint that produces a valid data file in a supported format. For example, to bring in data from [Google Sheets](https://www.google.com/sheets/about/) as a CSV file directly into Rill as a source ([leveraging the direct download link syntax](https://www.highviewapps.com/blog/how-to-create-a-csv-or-excel-direct-download-link-in-google-sheets/)), you can create a `source_name.yaml` file in the `sources` directory of your Rill project with the following content.
:::note Prerequisite
The Google Sheet must be a public sheet with `Anyone with the link` enabled.
:::
```yaml
type: model
connector: "duckdb"
sql: "select * from read_csv_auto('https://docs.google.com/spreadsheets/d//export?format=csv&gid=', normalize_names=True)"
```
:::note Updating the URL
Make sure to replace `SPREADSHEET_ID` and `SHEET_ID` with the ID of your spreadsheet and tab, respectively (which you can obtain from the URL when Google Sheets is open).
:::

:::tip `gsheets` DuckDB Community Extension
In cases where setting the Google Sheet to `Anyone with the link` is not allowed, DuckDB has an extension that allows you to share the sheet to a Google service account. However, we have not yet implemented the DuckDB community extension in our deployment of DuckDB. Please contact us if you are interested in using this feature.
:::
---
####### Apache Iceberg
---
title: Apache Iceberg
description: Read Iceberg tables from object storage
sidebar_label: Apache Iceberg
sidebar_position: 27
---
## Overview
[Apache Iceberg](https://iceberg.apache.org/) is an open table format for large analytic datasets. Rill supports reading Iceberg tables directly from object storage through compatible query engine integrations. Today, this is powered by DuckDB's native [Iceberg extension](https://duckdb.org/docs/extensions/iceberg/overview.html).
:::note Direct file access only
Rill reads Iceberg tables by scanning the table's metadata and data files directly from object storage. Catalog-based access (e.g., through a Hive Metastore, AWS Glue, or REST catalog) is not currently supported.
:::
## Storage Backends
Iceberg tables can be read from any of the following storage backends:
| Backend | URI format | Authentication |
|---|---|---|
| Amazon S3 | `s3://bucket/path/to/table` | Requires an [S3 connector](/developers/build/connectors/data-source/s3) |
| Google Cloud Storage | `gs://bucket/path/to/table` | Requires a [GCS connector](/developers/build/connectors/data-source/gcs) with HMAC keys |
| Azure Blob Storage | `azure://container/path/to/table` | Requires an [Azure connector](/developers/build/connectors/data-source/azure) |
| Local filesystem | `/path/to/table` | No authentication needed |
For cloud storage backends, you must first configure the corresponding storage connector with valid credentials. Rill uses these credentials to authenticate when reading the Iceberg table files.
## Using the UI
1. Click **Add Data** in your Rill project
2. Select **Apache Iceberg** as the data source type
3. Choose your storage backend (S3, GCS, Azure, or Local)
4. Enter the path to your Iceberg table directory
5. Optionally configure advanced parameters (allow moved paths, snapshot version)
6. Enter a model name and click **Create**
For cloud storage backends, the UI will prompt you to set up the corresponding storage connector if one doesn't already exist.
## Manual Configuration
Create a model that uses DuckDB's `iceberg_scan()` function to read the table.
### Reading from S3
Create `models/iceberg_data.yaml`:
```yaml
type: model
connector: duckdb
create_secrets_from_connectors: s3
materialize: true
sql: |
SELECT *
FROM iceberg_scan('s3://my-bucket/path/to/iceberg_table')
```
### Reading from GCS
:::info HMAC keys required
DuckDB's `iceberg_scan()` authenticates to GCS using HMAC keys, not JSON service account credentials. When configuring your [GCS connector](/developers/build/connectors/data-source/gcs), use the `key_id` and `secret` (HMAC) properties instead of `google_application_credentials`.
:::
```yaml
type: model
connector: duckdb
create_secrets_from_connectors: gcs
materialize: true
sql: |
SELECT *
FROM iceberg_scan('gs://my-bucket/path/to/iceberg_table')
```
### Reading from Azure
```yaml
type: model
connector: duckdb
create_secrets_from_connectors: azure
materialize: true
sql: |
SELECT *
FROM iceberg_scan('azure://my-container/path/to/iceberg_table')
```
### Reading from local filesystem
```yaml
type: model
connector: duckdb
materialize: true
sql: |
SELECT *
FROM iceberg_scan('/path/to/iceberg_table')
```
## Optional Parameters
The `iceberg_scan()` function accepts additional parameters:
| Parameter | Type | Description |
|---|---|---|
| `allow_moved_paths` | boolean | Allow reading tables where data files have been moved from their original location. Defaults to `true` in the UI. |
| `version` | string | Read a specific Iceberg snapshot version instead of the latest. |
Example with optional parameters:
```sql
SELECT *
FROM iceberg_scan('s3://my-bucket/path/to/iceberg_table',
allow_moved_paths = true,
version = '2')
```
## Deploy to Rill Cloud
Since Iceberg tables are read through DuckDB using your existing storage connector credentials, deploying to Rill Cloud follows the same process as the underlying storage connector:
- **S3**: Follow the [S3 deployment guide](/developers/build/connectors/data-source/s3#deploy-to-rill-cloud)
- **GCS**: Follow the [GCS deployment guide](/developers/build/connectors/data-source/gcs#deploy-to-rill-cloud)
- **Azure**: Follow the [Azure deployment guide](/developers/build/connectors/data-source/azure#deploy-to-rill-cloud)
Ensure your storage connector credentials are configured in your Rill Cloud project before deploying.
## Limitations
- **Direct file access only**: Rill reads Iceberg metadata and data files directly from storage. Catalog integrations (Hive Metastore, AWS Glue, REST catalog) are not supported.
- **DuckDB engine**: Iceberg support is currently provided through DuckDB's Iceberg extension. Additional engine support (e.g., ClickHouse) is planned.
- **GCS requires HMAC keys**: DuckDB's `iceberg_scan()` only supports HMAC authentication for GCS, not JSON service account credentials.
- **Read-only**: Rill reads from Iceberg tables but does not write to them.
---
####### Kafka
---
title: Kafka
description: Connect Kafka to OLAP Engines
sidebar_label: Kafka
sidebar_position: 30
---
:::warning For Customers with Druid/ClickHouse Engines Only
Note: The setup instructions below are for customers using Rill's hosted OLAP solution.
:::
## Setup Instructions
Follow the instructions below to grant Rill access to your Apache Kafka Cluster and the data on a given topic within the cluster. Providing access to a cloud-provided service, such as Confluent Cloud, is easier because all of the connection and security is already taken care of for you. If you are using a self-managed cluster, ensure security and encryption are configured accordingly.
The following scenarios are covered. Apache Kafka is designed to work within almost any enterprise environment, allowing for a variety of unique configurations. Work with your engineering team and Rill Data to ensure your configuration and setup are performant and secure.
* Confluent Cloud
* Private Kafka Cluster
* VPC Kafka Peering
* Public Kafka Cluster
* AWS Kinesis
### Confluent Cloud
Confluent Cloud is inherently secure and accessible. Communication with Confluent Cloud can be set up in a few minutes. The main consideration is to determine the level of accessibility for the credentials you will be using from the Rill platform to access the Confluent Cloud Cluster.
The concepts here apply to other Apache Kafka SaaS offerings, even though the means to access them might be different. Please reach out if you have any questions about integrating with your Apache Kafka SaaS provider.
### Credentials
The first step is to create a security key you can use from Rill Data to access your Kafka cluster. Select a granular access key to reduce exposure. You can use an existing key or an actual account when creating the client API access.
#### Create Key and Secret
Select "+ Add Key" from the "Cloud API Keys" menu option and create a non-admin account. Create a service account unique to accessing data from Rill Data and be sure to download and secure the client key and secret for use from Rill Data Druid Ingestion.
It is recommended to create a unique service account so your access control can be uniquely established for access from Rill Data into your Confluent Cloud Kafka Cluster.
If you fail to download or lose the key/secret, a new key/secret will need to be generated.
#### Create Cluster API Access
Rill Data manages the Kafka topic offset internally, so the Apache Kafka connection only needs topic read access.
The best way to provide Rill access to your cluster is through granular access with the above service account.
Ideally, using a topic-name prefix is preferred, as it minimizes the number of ACL rules you need to create and manage for the API access key/secret.
#### Data Ingestion
The key aspects for Confluent Cloud ingestion are establishing the bootstrap server, customer properties for access, and the topic to consume from.
- Bootstrap servers: *Cluster's Broker and Port*
- Topic: *The Kafka Topic*
- Consumer properties: *At minimum, the properties needed to access the cluster with key/secret*
- Where should the data be sampled from? *Start of stream* or *End of stream*
*See **ioConfig** example shown from a complete ingestion specification for pulling data from Kafka. Insert the **key** and **secret** into the JAAS config.*
```json
"ioConfig": {
"type": "kafka",
"consumerProperties": {
"bootstrap.servers": "{{ CLUSTER_HOSTNAME }}:9092",
"security.protocol": "SASL_SSL",
"sasl.jaas.config": "org.apache.kafka.common.security.plain.PlainLoginModule required username='{{ CLIENT_KEY }}' password='{{ CLIENT_SECRET }}';",
"sasl.mechanism": "PLAIN"
},
"topic": "rilldata-sourcedata",
"inputFormat": {
"type": "json"
},
"useEarliestOffset": true
}
```
### Private Kafka Cluster
When connecting to a private Apache Kafka Cluster, accessibility and security are the most significant configuration areas. Rill Data connects directly to the Kafka Cluster as a Kafka client and will access your cluster as any other consumer client.
* For Apache Kafka to be highly performant, the client API communicates directly to the active Kafka broker for a given partition.
* Configure your cluster so the brokers are each individually accessible from the Rill services.
* Apache Kafka has the server property **advertised.listeners** to ensure that the client has the correct information to communicate with an individual broker.
When configuring your Kafka Cluster, ensure it can be accessed from Rill by establishing a VPC. An example setup is shown via AWS Private Link below. AWS Private Link allows exposure of the Kafka brokers over a Network Load Balancer using VPC Endpoints. The network packets always remain within the AWS Network.
#### AWS Private Link
Kafka **EXTERNAL** advertised listeners should match the DNS name configured.
Network Load Balancer will expose all the brokers with different ports. Each broker can have an incremented port for **EXTERNAL** access, e.g., 9092, 9093, and 9094.
```shell
listener.security.protocol.map=EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT
advertised.listeners=PLAINTEXT://:9092,EXTERNAL://${PRIVATE_DNS_NAME}:${INCREMENTED_PORT}
## Eg:
# Broker #1
# advertised.listeners=PLAINTEXT://:9092,EXTERNAL://private-kafka.rilldata.com:19092
# Broker #2
# advertised.listeners=PLAINTEXT://:9092,EXTERNAL://private-kafka.rilldata.com:19093
# Broker #3
# advertised.listeners=PLAINTEXT://:9092,EXTERNAL://private-kafka.rilldata.com:19094
```
#### AWS Private Link using CloudFormation
1. Open AWS CloudFormation to create a new Stack.
2. Use the Amazon S3 URL: https://s3.amazonaws.com/cf-templates.rilldata.com/rilldata-private-link.yaml
3. Specify stack details:
* **Stack Name**: `rilldata-privatelink`
* **AccountId**: Rill Data AWS Account ID.
* **NlbArn**: ARN of the Network Load Balancer (Internal) through which we can share the internal Endpoints
4. Click Next, then Next again, acknowledge the capabilities, and create the stack.
5. You can check the events and it should create the resources for you.
6. Share the outputs with Rill Data.
We would be using the following CloudFormation Template.
```yaml title="YAML"
AWSTemplateFormatVersion: 2010-09-09
Metadata:
License: Apache-2.0
Description: 'AWS CloudFormation Template for creating a Private Link for a given Network Load Balancer'
Parameters:
NlbArn:
Type: String
Description: ARN of the Network Load Balancer
Default: arn:aws:elasticloadbalancing:us-east-1:248432388601:loadbalancer/net/kafka-broker/de46ce872b289b14
AccountId:
Type: String
Description: ID of the account to share the private link with.
Default: 417306524257
Resources:
EndpointService:
Type: AWS::EC2::VPCEndpointService
Properties:
AcceptanceRequired: True
NetworkLoadBalancerArns:
- !Ref NlbArn
EndpointServicePermissions:
Type: AWS::EC2::VPCEndpointServicePermissions
Properties:
AllowedPrincipals:
- !Join
- ''
- - 'arn:aws:iam::'
- !Ref AccountId
- ':root'
ServiceId: !Ref EndpointService
Outputs:
PrivateLinkServiceId:
Value: !Ref EndpointService
Description: Service ID of the Private Link
```
### VPC Peering
* Since the cluster is private, setting up SSL may not be needed -- as it would depend on your corporate policies.
* Authentication: create a user to be used for consuming
* Authorization: allow the user to consume from a specific topic
* Do VPC network peering
* Make sure every broker’s advertised endpoint is accessible from a consumer.
* Set up the consumer by storing the user’s credentials into the consumer’s configuration file.
### Public Kafka Cluster
By default, there is no encryption, authentication, or ACLs (access control list) configured. Any client can communicate to Kafka brokers via the PLAINTEXT port.
:::danger Kafka Security
It is critical that access via this port is restricted to trusted clients only. Network segmentation and/or authorization ACLs can be used to restrict access to trusted IPs in such cases.
:::
If neither is used, the cluster is wide open and can be accessed by anyone.
* **Enable SSL encryption**: SSL uses private-key/certificate pairs which are used during the SSL handshake process.
* **Authentication & Authorization**: Create a user to be used for consuming and allow the user to consume from a specific topic.
* Make sure every broker’s advertised endpoint is accessible from a consumer.
* Set up the consumer by adding the broker’s certificate into a trust store and storing the user’s credentials into the consumer’s configuration file.
### AWS Kinesis
We can provide access to the Kinesis stream through an IAM Role which will be assumed by the Rill Data AWS Account to gain access.
:::info Rill Data AWS Account
arn:aws:iam::248432388601:root
:::
#### Using CloudFormation Console
1. Open AWS CloudFormation to create a new Stack.
2. Use the Amazon S3 URL: `https://s3.amazonaws.com/cf-templates.rilldata.com/rilldata-kinesis-access.yaml`
3. Specify Stack Details:
* **Stack Name**: `rilldata-kinesis-access`
* **KinesisARN**: Name of the bucket we want to provide access to.
4. Click Next, then Next again, acknowledge the capabilities, and create the stack.
5. You can check the events and it should create the resources for you.
6. Share the outputs with Rill Data.
##### CloudFormation Template Reference
We would be using the following CloudFormation Template.
```yaml
AWSTemplateFormatVersion: '2010-09-09'
Metadata:
License: Apache-2.0
Description: 'AWS CloudFormation Template for providing Rill Data Access to Kinesis. It creates a
Role that can be assumed by the Rill Data AWS Account. The Role has an IAM policy associated with it.'
Parameters:
KinesisARN:
Type: String
Description: Kinesis Stream ARN
NamePrefix:
Type: String
Description: Name prefix for the IAM Policy and IAM Role.
Default: rilldata
Resources:
KinesisRole:
Type: AWS::IAM::Role
Properties:
Description: 'Rill Data Access to the Kinesis. Managed by: CloudFormation'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
AWS:
- 'arn:aws:iam::248432388601:root'
Action:
- 'sts:AssumeRole'
Policies:
- PolicyName: !Join
- ''
- - !Ref NamePrefix
- 'KinesisAccessPolicy'
PolicyDocument:
Statement:
- Effect: Allow
Action:
- "kinesis:Describe*"
- "kinesis:List*"
- "kinesis:Get*"
Resource:
- !Ref KinesisARN
RoleName: !Join
- '-'
- - !Ref NamePrefix
- kinesis-access
Tags:
- Key: Accessor
Value: RillData
- Key: ManagedBy
Value: CloudFormation
Outputs:
RoleName:
Value: !GetAtt [KinesisRole, Arn]
Description: Kinesis Access Role Arn, to be shared with Rill Data
```
## References
### SSL Encryption
SSL uses private-key/certificate pairs which are used during the SSL handshake process.
* Each broker needs its own private-key/certificate pair, and the client uses the certificate to authenticate the broker.
* Each logical client needs a private-key/certificate pair if client authentication is enabled, and the broker uses the certificate to authenticate the client.
```shell
# Configure the password, truststore, and keystore
# Since this stores passwords directly in the broker configuration file,
# it is important to restrict access to these files via file system permissions.
ssl.truststore.location=/var/ssl/private/kafka.server.truststore.jks
ssl.truststore.password=test1234
ssl.keystore.location=/var/ssl/private/kafka.server.keystore.jks
ssl.keystore.password=test1234
ssl.key.password=test1234
# Enable SSL for inter-broker communication, add the following to the broker properties file (it defaults to PLAINTEXT):
security.inter.broker.protocol=SSL
```
### Authorization
Kafka supports client authentication via SASL. SASL authentication can be enabled concurrently with SSL encryption. The supported SASL mechanisms are:
* GSSAPI (Kerberos)
* OAUTHBEARER
* SCRAM
* PLAIN
* Delegation Tokens
#### JAAS configurations
Kafka uses the Java Authentication and Authorization Service (JAAS) for SASL configuration.
Provide JAAS configurations for all SASL authentication mechanisms. Brokers can configure JAAS by passing a static JAAS configuration file into the JVM using the java.security.auth.login.config property at runtime.
```shell
export KAFKA_OPTS="-Djava.security.auth.login.config=/etc/kafka/kafka_server_jaas.conf"
bin/kafka-server-start etc/kafka/server.properties
```
Brokers can also configure JAAS using the broker configuration property sasl.jaas.config.
Prefix the property name with the listener prefix, including the SASL mechanism, i.e., listener.name.```listenerName```.```saslMechanism``````.sasl.jaas.config.
Specify one login module in the config value. To configure multiple mechanisms on a listener, you must provide a separate config for each mechanism using the listener and mechanism prefix.
```shell
listener.name.sasl_ssl.scram-sha-256.sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required \
username="admin" \
password="admin-secret";
listener.name.sasl_ssl.plain.sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required \
username="admin" \
password="admin-secret" \
user_admin="admin-secret" \
user_alice="alice-secret";
```
This is the preferred method of configuring JAAS for brokers.
### Authorization using ACLs
Kafka ships with a pluggable, out-of-the-box Authorizer implementation that uses ZooKeeper to store all the ACLs. It is important to set ACLs because otherwise access to resources is limited to super users when an Authorizer is configured. The default behavior is that if a resource has no associated ACLs, then no one is allowed to access the resource, except super users.
#### Broker Configuration
##### Authorizer
To enable ACLs, you must configure an Authorizer. Kafka provides a simple authorizer implementation, and to use it, you can add the following to server.properties:
```
authorizer.class.name=kafka.security.auth.SimpleAclAuthorizer
```
##### Super Users
By default, if no resource patterns match a specific resource, then the resource has no associated ACLs, and therefore no one other than super users are allowed to access the resource. If you want to change that behavior, you can include the following in server.properties:
```
allow.everyone.if.no.acl.found=true
super.users=User:Bob;User:Alice
```
---
####### Local File
---
title: Local File
description: Connect to your local data files
sidebar_label: Local File
sidebar_position: 35
---
Import data from files stored on your local machine into your Rill project.
## Overview
The Local File connector allows you to import data from CSV, JSON, Parquet, and other supported file formats directly from your local filesystem. This is perfect for working with local datasets, exports, or files you've downloaded.
## Adding a Local File Source
### Option 1: Using the Rill UI
1. In the left navigation pane, click the **"+"** button next to **Sources**
2. Select **"Local File"** from the connector options
3. Navigate to and select your file, or drag and drop it directly onto the Rill interface
### Option 2: Using Code
Create a YAML configuration file in your project's `sources` directory:
```yaml
# Model YAML
# Reference documentation: https://docs.rilldata.com/reference/project-files/models
type: model
materialize: true
connector: duckdb
sql: "select * from read_csv('data/your_file_name.csv', auto_detect=true, ignore_errors=1, header=true)
```
**Important:** When using relative paths, they should be relative to your Rill project root (where `rill.yaml` is located), not the `sources` directory.
## Importing Multiple Files
Use glob patterns to import data from multiple files at once:
```yaml
type: source
connector: local_file
path: /path/to/data/*.csv
```
**Examples:**
- `data/*.csv` - All CSV files in the data directory
- `exports/2024-*.parquet` - All Parquet files from 2024
- `logs/**/*.json` - All JSON files in logs and subdirectories
For detailed glob pattern syntax, refer to the [DuckDB multiple files documentation](https://duckdb.org/docs/stable/data/multiple_files/overview.html).
## Supported File Formats
The Local File connector supports various file formats including:
- CSV
- JSON
- Parquet
:::warning File Size Limits
When ingesting the Data into Rill, you'll notice a new `/data` folder path with a copy of the CSV file. This is designed so that when you publish to Rill Cloud, the file will also be included. Note that there is a 100MB limit to each unique file. Files over 100MB will not be deployed with your project.
:::
---
####### Salesforce
---
title: Salesforce
description: Connect to data in a Salesforce org using the Bulk API
sidebar_label: Salesforce
sidebar_position: 65
---
## Overview
[Salesforce](https://www.salesforce.com/) is a leading cloud-based Customer Relationship Management (CRM) platform designed to help businesses connect with and understand their customers better. It offers a comprehensive suite of applications focused on sales, customer service, marketing automation, analytics, and application development. Salesforce enables organizations of all sizes to build stronger relationships with their customers through personalized experiences, streamlined communication, and predictive insights. Rill can ingest data from Salesforce as a source by utilizing the Bulk API, which requires a Salesforce username and password (and, in some cases, a token, depending on the org configuration) to authenticate against a Salesforce org.
## Local credentials
When using Rill Developer on your local machine, you will need to provide your credentials via a connector file. We would recommend not using plain text to create your file and instead use the `.env` file. For more details on your connector, see [connector YAML](/reference/project-files/connectors) for more details.
:::tip Updating the project environmental variable
If you've already deployed to Rill Cloud, you can either [push/pull the credential]( /guide/administration/project-settings/variables-and-credentials#pushing-and-pulling-credentials-to--from-rill-cloud-via-the-cli) from the CLI with:
```
rill env push
rill env pull
```
:::
Alternatively, you can include the credentials directly in the underlying source YAML by adding the `username` and `password` parameters. For example, your source YAML may contain the following properties (these can also be configured through the UI during source creation):
```yaml
type: "model"
connector: "salesforce"
endpoint: "login.salesforce.com"
username: "user@example.com"
password: "MyPasswordMyToken"
soql: "SELECT Id, Name, CreatedDate FROM Opportunity"
sobject: "Opportunity"
```
:::tip Did you know?
If this project has already been deployed to Rill Cloud and credentials have been set for this source, you can use `rill env pull` to [pull these cloud credentials](/developers/build/connectors/credentials/#rill-env-pull) locally (into your local `.env` file). Please note that this may override any credentials you have set locally for this source.
:::
## Deploy to Rill Cloud
When deploying a project to Rill Cloud, Rill requires you to explicitly provide Salesforce credentials used in your project. Please refer to our [connector YAML reference docs](/reference/project-files/connectors) for more information.
If you subsequently add sources that require new credentials (or if you simply entered the wrong credentials during the initial deploy), you can update the credentials by pushing the `Deploy` button to update your project or by running the following command in the CLI:
```
rill env push
```
:::note
Leave the `key` and `client_id` fields blank unless you are using JWT (described in the next section [below](#jwt)).
:::
### JWT
Authentication using JWT instead of a password is also supported. Set `client_id` to the **Client Id** (also known as the _Consumer Key_) of the Connected App to use, and set `key` to contain the PEM-formatted private key to use for signing.
---
####### SQLite
---
title: SQLite
description: Connect to data in SQLite
sidebar_label: SQLite
sidebar_position: 80
---
## Overview
[SQLite](https://www.sqlite.org/about.html) is a lightweight, self-contained SQL database engine renowned for its reliability, speed, and full-featured, serverless architecture. SQLite is primarily known as an in-process database and is widely used in embedded systems, mobile applications, and various small-to-medium-sized applications due to its simplicity, zero-configuration, and single-file database format. SQLite supports standard SQL syntax and includes features such as transactions and atomic commit and rollback, making it a practical choice for applications requiring a compact, efficient data management system. Rill supports connecting to and reading from a SQLite database as a source through the [DuckDB SQLite extension](https://duckdb.org/docs/extensions/sqlite.html).
## Connect to SQLite
SQLite databases are read through DuckDB's [SQLite extension](https://duckdb.org/docs/extensions/sqlite.html) using the `sqlite_scan()` function. No separate connector is needed.
Create a model file (e.g., `models/my_sqlite_data.yaml`):
```yaml
type: model
connector: duckdb
materialize: true
sql: |
SELECT *
FROM sqlite_scan('data/mydatabase.db', 'my_table')
```
:::tip
If you plan to deploy the project to Rill Cloud, place the SQLite database file in a `data` folder in your Rill project directory and use the relative path (e.g., `data/mydatabase.db`).
:::
## Deploy to Rill Cloud
Once a project with a SQLite source has been deployed using `rill deploy`, Rill Cloud will need to be able to access and retrieve the underlying database file. In most cases, this means that the corresponding SQLite database file should be included within a directory in your Git repository, which will allow you to specify a relative path in your source definition (from the project root).
:::warning When Using An External SQLite Database
If the SQLite database file is external to your Rill project directory, you will still be able to use the fully qualified path to read this SQLite database _locally_ using Rill Developer. However, when deployed to Rill Cloud, this source will throw an **error**.
:::
---
##### Olap
---
####### Google BigQuery
---
title: Google BigQuery
description: Power Rill dashboards using BigQuery
sidebar_label: Google BigQuery
sidebar_position: 13
---
[Google BigQuery](https://cloud.google.com/bigquery/docs) is a fully managed, serverless data warehouse that enables scalable and cost-effective analysis of large datasets. Rill supports connecting to BigQuery as a live connector, allowing you to build metrics views and dashboards directly on top of existing BigQuery tables — no data movement required.
:::tip BigQuery as a Live Connector vs. Data Source Connector
Rill supports BigQuery in two distinct modes:
- **OLAP (Live Connector)** — Rill queries BigQuery directly at dashboard load time. No data is ingested into Rill. Use this when your data is already modeled and optimized in BigQuery and you want Rill as a visual layer on top. Set `olap_connector: bigquery` in `rill.yaml`.
- **Data Source Connector** — Rill extracts data from BigQuery and ingests it into its embedded engine (DuckDB or ClickHouse). Use this when you want Rill to manage the data pipeline, apply transformations via SQL models, or combine BigQuery data with other sources. See the [BigQuery data source docs](/developers/build/connectors/data-source/bigquery).
In general, use the live connector if your BigQuery tables are already production-ready and large. Use data source ingestion if you need to transform, join, or enrich the data before building dashboards.
:::
## Connect to BigQuery
After selecting "Add Data", select BigQuery and fill in your connection parameters. This will automatically create the `bigquery.yaml` file in your `connectors` directory and populate the `.env` file with your credentials.
For more information on supported parameters, see our [BigQuery connector YAML reference docs](/reference/project-files/connectors#bigquery).
```yaml
type: connector
driver: bigquery
project_id: "my-gcp-project"
google_application_credentials: "{{ .env.GOOGLE_APPLICATION_CREDENTIALS }}"
```
:::tip Creating a Service Account
For production use, authenticate with a Google Cloud service account JSON key. The service account needs the `roles/bigquery.dataViewer`, `roles/bigquery.readSessionUser`, and `roles/bigquery.jobUser` roles. See [the appendix in the BigQuery data source docs](/developers/build/connectors/data-source/bigquery#how-to-create-a-service-account-using-the-google-cloud-console) for a step-by-step guide.
:::
### Application Default Credentials (Local Development)
For local development, you can omit `google_application_credentials` and Rill will use your local Google Cloud CLI credentials:
```bash
gcloud auth application-default login
```
```yaml
type: connector
driver: bigquery
project_id: "my-gcp-project"
```
:::warning Not suitable for Rill Cloud
Application Default Credentials only work locally. When deploying to Rill Cloud, always provide a service account JSON via `google_application_credentials`.
:::
### Controlling Query Costs
BigQuery bills based on bytes scanned. On the on-demand pricing model, we recommend setting `max_bytes_billed` on the connector to cap the amount scanned per dashboard query. Queries that exceed the limit fail with an error instead of running.
```yaml
type: connector
driver: bigquery
project_id: "my-gcp-project"
google_application_credentials: "{{ .env.GOOGLE_APPLICATION_CREDENTIALS }}"
max_bytes_billed: 100000000000 # 100 GB
```
`max_bytes_billed` only applies to live connector dashboard queries. It is not enforced when ingesting data via the BigQuery data source connector. The default value of `0` disables the limit.
## Build a Metrics View on BigQuery
Once connected, set BigQuery as the OLAP connector in your `rill.yaml`:
```yaml
olap_connector: bigquery
```
Then create a metrics view that references a BigQuery table:
```yaml
type: metrics_view
connector: bigquery
database: my-gcp-project # BigQuery project (optional; defaults to the connector's project_id)
database_schema: my_dataset # BigQuery dataset
model: my_table
timeseries: created_at
dimensions:
- column: region
- column: category
measures:
- name: total_revenue
expression: SUM(revenue)
```
In BigQuery terminology, `database` maps to the **project**, `database_schema` maps to the **dataset**, and `model` maps to the **table**. Measure expressions must use [BigQuery SQL](https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax) syntax.
:::note
Rill supports metrics views directly on BigQuery as a live connector. Incremental models and partitioned ingestion are not supported in live connector mode.
:::
To reduce bytes scanned on dashboards with repeat traffic, see [Caching query results](/developers/build/metrics-view/underlying-model#caching-query-results) on live connectors. Caching pairs well with [`max_bytes_billed`](#controlling-query-costs) — use both to cap per-query cost and reduce overall query volume.
---
####### ClickHouse
---
title: ClickHouse
description: Power Rill dashboards using ClickHouse
sidebar_label: ClickHouse
sidebar_position: 0
---
import LoomVideo from '@site/src/components/LoomVideo'; // Adjust the path as needed
[ClickHouse](https://clickhouse.com/docs/en/intro) is an open-source, column-oriented OLAP database management system known for its ability to perform real-time analytical queries on large-scale datasets. Its architecture is optimized for high performance, leveraging columnar storage and advanced compression techniques to speed up data reads and significantly reduce storage costs. ClickHouse's efficiency in query execution, scalability, and ability to handle even petabytes of data make it an excellent choice for real-time analytic use cases.
Rill supports connecting to an existing ClickHouse cluster via a "live connector" and using it as an OLAP engine built against [external tables](/developers/build/connectors/olap#external-olap-tables) to power Rill dashboards. This is particularly useful when working with extremely large datasets (hundreds of GBs or even TB+ in size).
:::note Supported Versions
Rill supports connecting to ClickHouse v22.7 or newer versions.
:::
## Connect to ClickHouse
When using ClickHouse for local development, you can connect via connection parameters or by using the DSN. Both local instances of ClickHouse and ClickHouse Cloud are supported.
After selecting "Add Data", select ClickHouse and fill in your connection parameters. This will automatically create the `clickhouse.yaml` file in your `connectors` directory and populate the `.env` file with `CLICKHOUSE_PASSWORD` or `CLICKHOUSE_DSN` depending on which you select in the UI.
For more information on supported parameters, see our [ClickHouse connector YAML reference docs](/reference/project-files/connectors#clickhouse).
```yaml
type: connector
driver: clickhouse
host:
port:
username:
password: "{{ .env.CLICKHOUSE_PASSWORD }}"
ssl: true # required for ClickHouse Cloud
```
After creating the connector, you can edit the `.env` file manually in the project directory, or the connectors/clickhouse.yaml file.
:::tip Getting DSN errors in dashboards after setting `.env`?
If you are facing issues related to DSN connection errors in your dashboards even after setting the connection string via the project's `.env` file, try restarting Rill using the `rill start --reset` command.
:::
### Connection String (DSN)
Rill is able to connect to ClickHouse using the [ClickHouse Go Driver](https://clickhouse.com/docs/en/integrations/go). An appropriate connection string (DSN) will need to be set through the `CLICKHOUSE_DSN` property in Rill.
```bash
CLICKHOUSE_DSN="clickhouse://:?username=&password="
```
Once the file is created, it will be added directly to the `.env` file in the project directory. To make changes to this connector, modify `CLICKHOUSE_DSN`.
```yaml
type: connector
driver: clickhouse
dsn: "{{ .env.CLICKHOUSE_DSN }}"
```
:::info Check your port
In most situations, the default port is 9440 for TLS and 9000 when not using TLS. However, it is worth double-checking the port that your ClickHouse instance is configured to use when setting up your connection string.
:::
:::note DSN properties
For more information about available DSN properties and setting an appropriate connection string, please refer to ClickHouse's [documentation](https://github.com/ClickHouse/clickhouse-go?tab=readme-ov-file#dsn).
:::
## Connect to ClickHouse Cloud
If you are connecting to an existing [ClickHouse Cloud](https://clickhouse.com/cloud) instance, you can retrieve connection details about your instance by clicking on the `Connect` tab from within the admin settings navigation page. This will provide relevant information, such as the hostname, port, and username being used for your instance that you can then use to construct your DSN.

Using the information in the ClickHouse UI, populate the parameters of your connection.
### Connection String (DSN)
Because ClickHouse Cloud requires a secure connection over [https](https://github.com/ClickHouse/clickhouse-go?tab=readme-ov-file#http-support-experimental), you will need to pass in `secure=true` and `skip_verify=true` as additional URL parameters as part of your https URL (for your DSN).
```yaml
https://:?username=&password=&secure=true&skip_verify=true
```
:::info Need help connecting to ClickHouse?
If you would like to connect Rill to an existing ClickHouse instance, please don't hesitate to [contact us](/contact). We'd love to help!
:::
## Rill Managed ClickHouse
By setting `managed: true` in your ClickHouse connector, you will enable an embedded ClickHouse server to spin up with Rill. This will allow you to import data directly into this ClickHouse server without having to worry about managing an external database.
```yaml
type: connector
driver: clickhouse
managed: true
```
Data ingestion features are not yet available in the UI, please refer to our [model documentation](/reference/project-files/models) on how to ingest data into ClickHouse. For a guide, see [ingesting data directly into ClickHouse](/developers/tutorials/rill-clickhouse/r_ch_ingest).
## Read Only Connector
You can configure your ClickHouse connector to operate in read-only mode by adding a mode parameter. This provides an additional security layer by ensuring your connector can only read data and cannot perform write operations.
```yaml
mode: read # readwrite
```
## Configuring Rill Cloud
When deploying a ClickHouse-backed project to Rill Cloud, you have the following options to pass the appropriate connection string to Rill Cloud:
1. If you have followed the UI to create your ClickHouse connector, the password or DSN should already exist in the .env file. During the deployment process, this `.env` file is automatically pushed with the deployment.
2. If `CLICKHOUSE_DSN` has already been set in your project `.env`, you can push and update these variables directly in your cloud deployment by using the `rill env push` command.
:::warning Local ClickHouse Server
If you are developing on a locally running ClickHouse server, this will not be deployed with your project. You will either need to use ClickHouse Cloud or Managed ClickHouse.
:::
## Setting the Default OLAP Connection
Creating a connection to an OLAP engine will automatically add the `olap_connector` property in your project's [rill.yaml](/reference/project-files/rill-yaml) and change the default OLAP engine to ClickHouse. Once this is changed, you'll notice that some of the UI features are removed as we currently do not support modeling and direct source ingestion in ClickHouse. However, this is behind a feature flag, [contact us](/contact) for more information!
```yaml
olap_connector: clickhouse
```
:::info Interested in using multiple OLAP engines in the same project?
Please see our [Using Multiple OLAP Engines](/developers/build/connectors/olap/multiple-olap) page.
:::
## Reading from Multiple Schemas
Rill supports reading from multiple schemas in ClickHouse from within the same project in Rill Developer, and all accessible tables (given the permission set of the underlying user) should automatically be listed in the lower left-hand tab, which can then be used to [create dashboards](/developers/build/dashboards).
## Caching Query Results
To reduce load on dashboards with repeat traffic, see [Caching query results](/developers/build/metrics-view/underlying-model#caching-query-results).
## Additional Notes
- At the moment, we do not officially support modeling with ClickHouse; however, this is available via a feature flag. If this is something you're interested in, please [contact us](/contact).
- For dashboards powered by ClickHouse, [measure definitions](/developers/build/metrics-view/#measures) are required to follow standard [ClickHouse SQL](https://clickhouse.com/docs/en/sql-reference) syntax.
- Because string columns in ClickHouse can theoretically contain [arbitrary binary data](https://github.com/ClickHouse/ClickHouse/issues/2976#issuecomment-416694860), if your column contains invalid UTF-8 characters, you may want to first cast the column by applying the `toValidUTF8` function ([see ClickHouse documentation](https://clickhouse.com/docs/en/sql-reference/functions/string-functions#tovalidutf8)) before reading the table into Rill to avoid any downstream issues.
---
####### Databricks
---
title: Databricks
description: Power Rill dashboards using Databricks
sidebar_label: Databricks
sidebar_position: 02
---
[Databricks](https://docs.databricks.com) is a unified data and AI platform built on top of Apache Spark and the lakehouse architecture, with Unity Catalog for governance and Databricks SQL for analytics. Rill supports connecting to Databricks as a live connector, allowing you to build metrics views and dashboards directly on top of existing Databricks tables — no data movement required.
:::tip Databricks as a Live Connector vs. Data Source Connector
Rill supports Databricks in two distinct modes:
- **OLAP (Live Connector)** — Rill queries Databricks directly at dashboard load time. No data is ingested into Rill. Use this when your data is already modeled and optimized in Databricks and you want Rill as a visual layer on top. Set `olap_connector: databricks` in `rill.yaml`.
- **Data Source Connector** — Rill extracts data from Databricks and ingests it into its embedded engine (DuckDB). Use this when you want Rill to manage the data pipeline, apply transformations via SQL models, or combine Databricks data with other sources. See the [Databricks data source docs](/developers/build/connectors/data-source/databricks).
In general, use the live connector if your Databricks tables are already production-ready and large. Use data source ingestion if you need to transform, join, or enrich the data before building dashboards.
:::
## Connect to Databricks
After selecting "Add Data", select Databricks and fill in your connection parameters. This will automatically create the `databricks.yaml` file in your `connectors` directory and populate the `.env` file with your credentials.
For more information on supported parameters, see our [Databricks connector YAML reference docs](/reference/project-files/connectors#databricks).
```yaml
type: connector
driver: databricks
host: "dbc-xxxxxxxx-xxxx.cloud.databricks.com"
http_path: "/sql/1.0/warehouses/xxxxxxxxxxxxxxxx"
token: "{{ .env.DATABRICKS_TOKEN }}"
catalog: "main" # optional
schema: "default" # optional
```
:::tip Finding your connection parameters
In the Databricks workspace, navigate to **SQL Warehouses**, select the warehouse you want to use, and open the **Connection details** tab. The **Server hostname** is your `host`, the **HTTP path** is your `http_path`, and you can generate a personal access token under **User Settings → Developer → Access tokens**.
:::
### Connection String (DSN)
For advanced configuration, you can specify a single DSN instead of the individual fields above. The DSN cannot be combined with `host`, `http_path`, `token`, `catalog`, or `schema`.
```yaml
type: connector
driver: databricks
dsn: "{{ .env.DATABRICKS_DSN }}"
```
```bash
# .env
DATABRICKS_DSN=token:dapi...@dbc-xxxxxxxx-xxxx.cloud.databricks.com:443/sql/1.0/warehouses/xxxxxxxxxxxxxxxx?catalog=main&schema=default
```
See the [Databricks SQL Go driver](https://github.com/databricks/databricks-sql-go) documentation for the full list of supported DSN parameters.
## Build a Metrics View on Databricks
Once connected, set Databricks as the OLAP connector in your `rill.yaml`:
```yaml
olap_connector: databricks
```
Then create a metrics view that references a Databricks table:
```yaml
type: metrics_view
connector: databricks
database: main # Unity Catalog (optional; defaults to the connector's catalog)
database_schema: my_schema # Schema within the catalog
model: my_table
timeseries: created_at
dimensions:
- column: region
- column: category
measures:
- name: total_revenue
expression: SUM(revenue)
```
In Databricks terminology, `database` maps to the **catalog**, `database_schema` maps to the **schema**, and `model` maps to the **table**. Measure expressions must use [Databricks SQL](https://docs.databricks.com/aws/en/sql/language-manual/) syntax.
:::note
Rill supports metrics views directly on Databricks as a live connector. Incremental models and partitioned ingestion are not supported in live connector mode.
:::
To reduce SQL warehouse spend on dashboards with repeat traffic, see [Caching query results](/developers/build/metrics-view/underlying-model#caching-query-results) on live connectors.
---
####### Druid
---
title: Druid
description: Power Rill dashboards using Druid
sidebar_label: Druid
sidebar_position: 05
---
[Apache Druid](https://druid.apache.org/docs/latest/design/) is an open-source, high-performance OLAP engine designed for real-time analytics on large datasets. It excels in analytical workloads due to its columnar storage format, which enables fast data aggregation, querying, and filtering. Druid is particularly well-suited for use cases that require interactive exploration of large-scale data, real-time data ingestion, and fast query responses, making it a popular choice for applications in business intelligence, user behavior analytics, and financial analysis.
Rill supports connecting to an existing Druid cluster via a "live connector" and using it as an OLAP engine built against [external tables](/developers/build/connectors/olap#external-olap-tables) to power Rill dashboards. This is particularly useful when working with extremely large datasets (hundreds of GBs or even TB+ in size).
## Configuring Rill Developer with Druid
When using Rill for local development, there are a few options to configure Rill to enable Druid as an OLAP engine:
1. Connect to an OLAP engine via Add Data. This will automatically create the `druid.yaml` file in your `connectors` directory and populate the `.env` file with `DRUID_PASSWORD` or `DRUID_DSN` depending on which you select in the UI.
For more information on supported parameters, see our [Druid connector YAML reference docs](/reference/project-files/connectors#druid).
```yaml
type: connector
driver: druid
host:
port:
username:
password: "{{ .env.DRUID_PASSWORD }}"
ssl: true
# or
dsn: "{{ .env.DRUID_DSN }}"
```
2. You can manually set `DRUID_DSN` in your project's `.env` file or try pulling existing credentials locally using `rill env pull` if the project has already been deployed to Rill Cloud.
:::tip Getting DSN errors in dashboards after setting `.env`?
If you are facing issues related to DSN connection errors in your dashboards even after setting the connection string via the project's `.env` file, try restarting Rill using the `rill start --reset` command.
:::
## Connection String (DSN)
Rill connects to Druid using the [HTTP API](https://druid.apache.org/docs/latest/api-reference/sql-api) and requires a connection string of the following format: `http://:@:/druid/v2/sql`. If `user` or `password` contain special characters, they should be URL encoded (i.e., `p@ssword` -> `p%40ssword`). This should be set in the `DRUID_DSN` property in Rill.
As an example, this typically looks like:
```bash
DRUID_DSN="https://user:password@localhost:8888/druid/v2/sql"
```
:::info Need help connecting to Druid?
If you would like to connect Rill to an existing Druid instance, please don't hesitate to [contact us](/contact). We'd love to help!
:::
## Setting the Default OLAP Connection
When connecting to Druid via the UI, the default OLAP connection will be automatically added to your rill.yaml. This will change the way the UI behaves, such as adding new data sources, as this is not supported with a Druid-backed Rill project.
```yaml
olap_connector: druid
```
:::note
For more information about available properties in `rill.yaml`, see our [project YAML](/reference/project-files/rill-yaml) documentation.
:::
:::info Interested in using multiple OLAP engines in the same project?
Please see our [Using Multiple OLAP Engines](/developers/build/connectors/olap/multiple-olap) page.
:::
## Configuring Rill Cloud
When deploying a Druid-backed project to Rill Cloud, you have the following options to pass the appropriate connection string to Rill Cloud:
1. If you have followed the UI to create your Druid connector, the password or DSN should already exist in the .env file. During the deployment process, this `.env` file is automatically pushed with the deployment.
2. If `DRUID_DSN` has already been set in your project `.env`, you can push and update these variables directly in your cloud deployment by using the `rill env push` command.
## Supported Versions
Rill supports connecting to Druid v28.0 or newer versions.
## Caching Query Results
To reduce broker load on dashboards with repeat traffic, see [Caching query results](/developers/build/metrics-view/underlying-model#caching-query-results) on live connectors.
## Additional Notes
- At the moment, we do not support modeling with Druid. If this is something you're interested in, please [contact us](/contact).
- For dashboards powered by Druid, [measure definitions](/developers/build/metrics-view/#measures) are required to follow standard [Druid SQL](https://druid.apache.org/docs/latest/querying/sql/) syntax.
---
####### DuckDB
---
title: DuckDB
description: Power Rill dashboards using DuckDB (default)
sidebar_label: DuckDB
sidebar_position: 10
---
[DuckDB](https://duckdb.org/why_duckdb.html) is an in-memory, columnar SQL database designed for analytical (OLAP) workloads, offering high-speed data processing and analysis. Its columnar storage model and vectorized query execution make it highly efficient for OLAP tasks, enabling fast aggregation, filtering, and joins on large datasets.
## Rill Managed DuckDB
By default, Rill includes DuckDB as an embedded OLAP engine that ingests data from [data sources](/developers/build/connectors) and powers your dashboards. When you start a new project, you'll see a `connectors/duckdb.yaml` file alongside other project files. No additional configuration is needed to use DuckDB with Rill Developer or Rill Cloud.
```yaml
type: connector
driver: duckdb
managed: true
```
:::tip Performance Considerations
DuckDB is an excellent analytical engine but can face performance challenges as data size grows significantly. As a general guideline, we recommend keeping your data size in DuckDB **under 50GB** along with other [performance recommendations](/developers/tutorials/performance). For larger datasets, Rill still provides excellent performance but may require additional backend optimizations. [Contact us](/contact) if you need assistance with large-scale deployments.
:::
## Live Connect to External DuckDB
Rill also supports connecting to external DuckDB database files as a "live connector". This allows you to leverage existing DuckDB databases within Rill to create metrics views and dashboards.
:::warning Local Development Only
This setup is designed for local development and testing only. It will not deploy to Rill Cloud under most circumstances because:
- Rill Cloud can only access files within your project directory
- If your DuckDB file is outside the project folder, it cannot be bundled for deployment
- Files larger than 100MB will fail to deploy due to upload size limits
For production deployments, consider using our [external DuckDB data source](/developers/build/connectors/data-source/duckdb) to ingest your data instead.
:::
### Configuration
Using the UI, select the DuckDB icon under the OLAP section to add a new DuckDB connector. Any existing connectors with data models will need to be refreshed to ingest the data into your external DuckDB.
```yaml
type: connector
driver: duckdb
path: '/path/to/main.db'
```
### Setting the Default OLAP Connection
Creating a connection to MotherDuck will automatically add the `olap_connector` property in your project's [rill.yaml](/reference/project-files/rill-yaml) and change the default OLAP engine to `duckdb`.
```yaml
olap_connector: duckdb
```
## Using DuckDB Extensions
DuckDB supports a wide variety of extensions that can enhance its functionality. To use extensions with Rill's embedded DuckDB, configure them in your connector:
```yaml
# connectors/duckdb.yaml
type: connector
driver: duckdb
init_sql: |
INSTALL httpfs;
LOAD httpfs;
INSTALL spatial;
LOAD spatial;
```
### Popular Extensions
For a complete list of available extensions, see the [DuckDB Extensions documentation](https://duckdb.org/docs/extensions/overview).
## Multiple OLAP Engines
While not recommended, Rill supports using multiple OLAP engines in a single project. For more information, see [Using Multiple OLAP Engines](/developers/build/connectors/olap/multiple-olap).
## Additional Notes
- For dashboards powered by DuckDB, [measure definitions](/developers/build/metrics-view/#measures) are required to follow standard [DuckDB SQL](https://duckdb.org/docs/sql/introduction) syntax.
- There is a known issue around creating a DuckDB source via the UI; you will need to create the YAML file manually.
---
####### DuckLake
---
title: DuckLake
description: Power Rill dashboards using DuckLake
sidebar_label: DuckLake
sidebar_position: 12
---
[DuckLake](https://ducklake.select/) is an open lakehouse format built on DuckDB. A DuckLake keeps table data as Parquet files in object storage (local, S3, GCS, or Azure) while the catalog (schemas, snapshots, statistics) lives in a separate database — DuckDB, SQLite, PostgreSQL, or MySQL. Rill connects to DuckLake through the DuckDB driver and uses it as a live OLAP engine, so no data is ingested into Rill and all queries are pushed down to DuckDB against your lake.
:::note DuckLake uses the DuckDB Driver
DuckLake connectors use `driver: duckdb` under the hood. The difference from a standard DuckDB connector is the `attach` clause, which points DuckDB at your DuckLake catalog and data path.
:::
## Configuring Rill Developer with DuckLake
Create the connector via **Add Data → DuckLake** in the UI. Rill will generate a `connectors/ducklake.yaml` file and set DuckLake as the default OLAP engine in `rill.yaml`.
```yaml
type: connector
driver: duckdb
attach: "'ducklake:metadata.ducklake' (DATA_PATH 'data/')"
```
The `attach` clause is passed directly to DuckDB's `ATTACH` statement. It must begin with the `ducklake:` metadata backend and should include a `DATA_PATH` pointing at the directory (local or object storage) that holds your Parquet files.
### Supported Metadata Backends
DuckLake can store its catalog in any of the following:
- **DuckDB** — `'ducklake:metadata.ducklake'`
- **SQLite** — `'ducklake:sqlite:metadata.sqlite'`
- **PostgreSQL** — `'ducklake:postgres:dbname=ducklake host=...'`
- **MySQL** — `'ducklake:mysql:host=... user=... database=ducklake'`
For cloud-hosted catalogs, credentials for the metadata database can be injected via environment variables, e.g. `host={{ .env.DUCKLAKE_PG_HOST }}`.
### Supported Data Paths
The `DATA_PATH` in your `attach` clause can point at either local storage or any object store supported by DuckDB:
- Local filesystem — `DATA_PATH 'data/'`
- S3 — `DATA_PATH 's3://my-bucket/ducklake/'`
- GCS — `DATA_PATH 'gs://my-bucket/ducklake/'`
- Azure Blob Storage — `DATA_PATH 'azure://my-container/ducklake/'`
See the [DuckLake docs](https://ducklake.select/docs/stable/duckdb/usage/connecting) for the full ATTACH syntax.
### Setting the Default OLAP Connection
Creating a DuckLake connector automatically sets `olap_connector` in your project's [rill.yaml](/reference/project-files/rill-yaml) to the new connector.
```yaml
olap_connector: ducklake
```
## Advanced Options
The `attach` clause is passed through to DuckDB and accepts the full set of DuckLake ATTACH options — see the [DuckLake ATTACH reference](https://ducklake.select/docs/stable/duckdb/usage/connecting) for the complete list.
Example with multiple options:
```yaml
type: connector
driver: duckdb
attach: "'ducklake:metadata.ducklake' (DATA_PATH 's3://my-bucket/ducklake/', OVERRIDE_DATA_PATH true, SNAPSHOT_VERSION '42')"
```
## Trying DuckLake Without Your Own Data
If you want to see DuckLake in action before pointing Rill at your own catalog, DuckDB hosts a public `lineitem` table from TPC-H (scale factor 3) as a read-only DuckLake. Load it in the [DuckDB browser visualizer](https://duckdb.org/visualizer/#resource_path=https%3A%2F%2Fblobs.duckdb.org%2Fdatalake%2Ftpch-sf3.ducklake&resource_type=ducklake&table_name=lineitem) to confirm the catalog is reachable, then point Rill at the same resource:
```yaml
type: connector
driver: duckdb
attach: "'ducklake:https://blobs.duckdb.org/datalake/tpch-sf3.ducklake'"
```
## Configuring Rill Cloud
When deploying a DuckLake-backed project to Rill Cloud:
1. Any secrets referenced in the `attach` clause (e.g. S3 credentials, Postgres passwords) should be set via `{{ .env.KEY_NAME }}` in your YAML and managed with the project `.env` file.
2. Use `rill env push` to sync local environment variables to your cloud deployment.
3. The `DATA_PATH` must be reachable from Rill Cloud — local filesystem paths will not deploy.
## Additional Notes
- DuckLake uses the same SQL dialect as DuckDB, so all standard DuckDB functions are available. [Measure definitions](/developers/build/metrics-view/#measures) should follow standard [DuckDB SQL](https://duckdb.org/docs/sql/introduction) syntax.
- Rill opens DuckLake in read-only mode by default. To allow Rill to create or modify tables in the lake, enable write mode in the connector advanced options.
- Combine DuckLake with [multiple OLAP engines](/developers/build/connectors/olap/multiple-olap) to power different dashboards from different catalogs in the same project.
:::info Need help connecting to DuckLake?
If you would like to connect Rill to DuckLake or need assistance with setup, please don't hesitate to [contact us](/contact). We'd love to help!
:::
---
####### MotherDuck
---
title: MotherDuck
description: Power Rill dashboards using MotherDuck
sidebar_label: MotherDuck
sidebar_position: 15
---
[MotherDuck](https://motherduck.com/) is a cloud-native DuckDB service that provides scalable analytics and data processing capabilities. Built on the same core engine as DuckDB, MotherDuck offers the familiar SQL interface and performance characteristics while adding cloud-native features like serverless compute, automatic scaling, and collaborative data sharing.

Rill supports connecting to MotherDuck and using it as an OLAP engine to power dashboards. This is particularly useful when you want the performance and SQL compatibility of DuckDB with the scalability and collaboration features of a cloud service.
:::note Supported Versions
Rill supports connecting to MotherDuck using the latest DuckDB-compatible drivers and protocols.
:::
## Getting Your MotherDuck Access Token
To connect to MotherDuck, you'll need a access token from your MotherDuck account:
1. Log in to your [MotherDuck account](https://motherduck.com/)
2. Navigate to the **Settings** section
3. Go to **Access Tokens**
4. Create a new access token or copy an existing one

:::warning Keep Your Token Secure
Your MotherDuck access token provides access to your data. Keep it secure and never commit it directly to version control. Always use environment variables or secure credential management.
:::
## Configuring Rill Developer with MotherDuck
Connect to your OLAP engine via Add Data. This will automatically create the motherduck.yaml file in your connectors folder and populate the .env file with `MOTHERDUCK_TOKEN`.
For more information on supported parameters, see our [MotherDuck connector YAML reference docs](/reference/project-files/connectors#motherduck).
```yaml
type: connector
driver: duckdb
token: "{{ .env.MOTHERDUCK_TOKEN }}"
path: "md:my_database"
schema_name: "my_schema"
```
### Setting the Default OLAP Connection
Creating a connection to MotherDuck will automatically add the `olap_connector` property in your project's [rill.yaml](/reference/project-files/rill-yaml) and change the default OLAP engine to `motherduck`.
```yaml
olap_connector: motherduck
```
:::info Interested in using multiple OLAP engines in the same project?
Please see our [Using Multiple OLAP Engines](/developers/build/connectors/olap/multiple-olap) page.
:::
## Configuring Rill Cloud
When deploying a MotherDuck-backed project to Rill Cloud, you have the following options to pass the appropriate access token:
1. If you have followed the UI to create your MotherDuck connector, the token should already exist in the `.env` file. During the deployment process, this `.env` file is automatically pushed with the deployment.
2. If `motherduck_token` has already been set in your project `.env`, you can push and update these variables directly in your cloud deployment by using the `rill env push` command.
## Caching Query Results
To increase performance on dashboards with repeat traffic, see [Caching query results](/developers/build/metrics-view/underlying-model#caching-query-results) on live connectors.
## Additional Notes
- MotherDuck uses the same SQL syntax as DuckDB, so all standard DuckDB functions and features are available
- For dashboards powered by MotherDuck, [measure definitions](/developers/build/metrics-view/#measures) should follow standard [DuckDB SQL](https://duckdb.org/docs/sql/introduction) syntax
:::info Need help connecting to MotherDuck?
If you would like to connect Rill to MotherDuck or need assistance with setup, please don't hesitate to [contact us](/contact). We'd love to help!
:::
---
####### Using Multiple OLAP Engines
---
title: Using Multiple OLAP Engines
description: Using multiple OLAP Engines to power dashboards in the same project
sidebar_label: Using Multiple OLAP Engines
sidebar_position: 50
---
If you have access to another OLAP engine (such as [ClickHouse](/developers/build/connectors/olap/clickhouse) or [Druid](/developers/build/connectors/olap/druid), you have the option to either:
- Create dedicated projects that are powered by one specific OLAP engine (default)
- Use different OLAP engines _in the same project_ to power separate dashboards
On this page, we will walk through how to configure the latter.
### Why Multiple OLAP Engines?
There could be reasons why you wish to configure multiple OLAP engines within the same project:
- You have data sources that differ greatly in size but which you want to use within the same project. As a rule of thumb, DuckDB handles datasets _up to 50GB quite well_ and is performant. For much larger datasets, you may want a more enterprise-grade OLAP engine powering specific dashboards.
- You have existing datasets/tables from other OLAP stores that you wish to use in Rill, which may already be optimized, and which you do not want to separately ingest into Rill. Instead, you would like to create dashboards off these tables directly and have the OLAP engine power them.
:::info Don't see an OLAP engine?
If there's an OLAP engine you're interested in that isn't available, please don't hesitate to [contact us](/contact). We'd love to hear from you and learn more!
:::
## Enabling Multiple OLAP Engines
To configure multiple OLAP engines, you'll want to leave the default OLAP engine as [DuckDB](/developers/build/connectors/olap/duckdb) in your project and configure dashboards that are powered by other OLAP engines individually (more on this below).
### Setting up your OLAP Engine connection string (DSN)
Before getting started, you'll need to first configure the appropriate connection string for each OLAP engine that you plan to use in Rill. Besides the built-in DuckDB OLAP engine, each OLAP engine should have its own `connector..dsn` variable that needs to be configured.
**For Rill Developer:**
- You can set these variables in your project's `.env` file or try pulling existing credentials locally using `rill env pull` if the project has already been deployed to Rill Cloud.
:::tip Getting DSN errors in dashboards after setting `.env`?
There might be instances where you've configured the project's `.env` file with the appropriate connection DSN strings but dashboards are still throwing errors. In these situations, try restarting Rill using the `rill start --reset` command.
:::
**For Rill Cloud:**
- Add the required `connector..dsn` parameters to your project's `.env`, and you can push these updated variables to your deployed project directly using `rill env push`.
### Configuring DuckDB as the default OLAP engine
Not much needs to be done here as _DuckDB is the inherent default OLAP engine_ that is used by Rill. However, in case a different `olap_connector` is set in the project's `rill.yaml` file, this property should either be removed and/or set back to `duckdb`.
```yaml
olap_connector: duckdb
```
:::note rill.yaml
For more information about available configurations for `rill.yaml`, please see our [Project YAML](/reference/project-files/rill-yaml) reference documentation.
:::
### Setting the OLAP Engine in the metrics view YAML
For each metrics view that is using a separate OLAP engine (other than the default), you'll want to set the `connector` and `table` properties in the underlying [metrics view YAML](/reference/project-files/metrics-views) configuration to the OLAP engine and corresponding [external table](/developers/build/connectors/olap#external-olap-tables) that exists in your OLAP store, respectively.
```yaml
type: metrics_view
title:
connector:
table:
...
```
---
####### "Bring Your Own OLAP Engine (Live Connector)"
---
title: "Bring Your Own OLAP Engine (Live Connector)"
description: Configure the OLAP engine used by Rill
sidebar_label: "OLAP Engines"
sidebar_position: 0
toc_max_heading_level: 3
className: connect-connect
---
import ConnectorIcon from '@site/src/components/ConnectorIcon';
Rill supports connecting directly to your own OLAP engine via a "live connector". In this mode, no data is ingested into Rill, and all compute is pushed down to the OLAP engine. Use this mode if you've already handled all of your modeling upstream and want to use Rill as your visual application layer.
:::tip Models on Live Connectors
Rill also offers the ability to ingest and create tables directly from a [data source](/developers/build/connectors/data-source) to your OLAP engine via the live connector, however you'll need to consider a few topics.
- **Use a test database** to avoid accidentally overwriting production data
- **Incremental processing and related queries are not supported**
- **Feature availability may vary** between different OLAP engines
:::
In order to connect Rill to your OLAP engine:
1. Create the connector via the UI.
2. [Create the YAML](/reference/project-files/connectors#olap-engines) and set the [default OLAP engine](/reference/project-files/rill-yaml#configuring-the-default-olap-engine) via the rill.yaml file.
:::note `olap_connector` in rill.yaml
When setting the OLAP Engine via the UI, the `olap_connector` key will automatically update the rill.yaml.
:::
## OLAP Engines
Rill supports the use of several different OLAP engines to power your dashboards, including:
### ClickHouse
### Databricks
### Druid
### DuckDB
### DuckLake
### Google BigQuery
### MotherDuck
### Pinot
### Snowflake
### StarRocks
}
content="High-performance columnar database for real-time analytics and data warehousing."
link="/developers/build/connectors/olap/clickhouse"
linkLabel="Learn more"
referenceLink="clickhouse"
/>
}
content="Lakehouse platform with native support for metrics views as a live connector."
link="/developers/build/connectors/olap/databricks"
linkLabel="Learn more"
referenceLink="databricks"
/>
}
content="Real-time analytics database designed for high-performance OLAP queries."
link="/developers/build/connectors/olap/druid"
linkLabel="Learn more"
referenceLink="druid"
/>
}
content="Add extra parameters to Rill's embedded DuckDB or connect your own."
link="/developers/build/connectors/olap/duckdb"
linkLabel="Learn more"
referenceLink="duckdb"
/>
}
content="Open lakehouse format built on DuckDB — catalog in SQL, data in Parquet."
link="/developers/build/connectors/olap/ducklake"
linkLabel="Learn more"
referenceLink="ducklake"
/>
}
content="Serverless cloud data warehouse with native support for metrics views as a live connector."
link="/developers/build/connectors/olap/bigquery"
linkLabel="Learn more"
referenceLink="bigquery"
/>
}
content="Cloud-native DuckDB service for scalable analytics and data processing."
link="/developers/build/connectors/olap/motherduck"
linkLabel="Learn more"
referenceLink="motherduck"
/>
}
content="Distributed OLAP datastore for real-time analytics and business intelligence."
link="/developers/build/connectors/olap/pinot"
linkLabel="Learn more"
referenceLink="pinot"
/>
}
content="Cloud data warehouse with native support for metrics views as a live connector."
link="/developers/build/connectors/olap/snowflake"
linkLabel="Learn more"
referenceLink="snowflake"
/>
}
content="Distributed OLAP datastore for real-time analytics and business intelligence."
link="/developers/build/connectors/olap/starrocks"
linkLabel="Learn more"
referenceLink="starrocks"
/>
:::note Additional OLAP Engines
Rill is continually evaluating additional OLAP engines that can be added. For a full list of OLAP engines that we support, refer to our [OLAP Engines](/developers/build/connectors/olap) page. If you don't see an OLAP engine that you'd like to use, please don't hesitate to [reach out](/contact)!
:::
## Multiple OLAP Engines in a Single Project
Rill supports the use of multiple OLAP engines in a single project with some limitations. For more detailed information, see our reference on [multiple OLAP engines](/developers/build/connectors/olap/multiple-olap). The basic use cases for multiple engines in a single project are:
1. Using Rill on top of already created and optimized tables from different OLAP sources.
2. Separating data based on size, as performance on different engines differs based on the size of the data.
## Externally Hosted Services
If you have a firewall in front of your externally hosted service, you will need to whitelist the IP addresses below. This will allow you to connect to/from your service once your project is deployed to Rill Cloud.
```
35.196.245.100
34.74.117.37
35.196.153.31
34.75.22.143
34.148.167.51
35.237.60.193
```
## What is OLAP?
OLAP (or Online Analytical Processing) is a computational approach designed to enable rapid, multidimensional analysis of large volumes of data. With OLAP, data is typically organized into cubes instead of traditional two-dimensional tables, which can facilitate complex queries and data analysis in a way that is significantly more efficient and user-friendly for analytical tasks. In particular, OLAP databases can be especially well suited for BI use cases that require deep, multidimensional analysis or real-time / user-facing analytics and applications. Additionally, many modern OLAP databases are optimized to ingest large volumes of data, execute low-latency queries with high throughput, and process billions of rows quickly with an emphasis on speed and efficiency in data retrieval.
Unlike traditional relational databases or data warehouses that are optimized for transaction processing (with a focus on CRUD operations), OLAP databases are designed for query speed and complex analysis. Rather than storing data in a row-oriented manner, optimizing for transactional efficiency and operational queries, most OLAP databases are columnar and use pre-aggregated multidimensional cubes to speed up analytical queries. This allows a broad range of ad hoc queries and analysis to be performed without needing predefined schemas that are tailored to specific queries, and it's this flexibility that enables the highly interactive slice-and-dice exploration of data that powers Rill dashboards. This paradigm allows OLAP to be particularly well-suited for organizations and teams that want to dive deep into and understand their data to support decision-making processes, where speed and flexibility in the actual data analysis are important.
:::info Want to see OLAP in action?
Check [here](https://www.rilldata.com/case-studies) to see examples of use cases that can be powered by OLAP.
:::
### External OLAP tables
Rill supports creating and powering dashboards using existing tables from alternative [OLAP engines](/developers/build/connectors/olap) that have been configured in a particular project. These tables are not managed by Rill—hence, external—but allow users to bring in separate tables or datasets that might already exist in another preferred OLAP database of choice. This prevents the need to unnecessarily ingest this data into Rill, especially if the table is already optimized for use by this other OLAP engine, and allows Rill to connect to the data directly (and submit analytical queries).

## Performance Tips
### Data Lifecycle Management
One common way to decrease overall data size and improve query performance (by scanning less data) is to roll up your data to higher time grains historically. Typically, this means taking hourly data and rolling up to daily data when the additional level of granularity is no longer necessary for business needs. Databases like Apache Druid have these lifecycle tools built in, or reach out to Rill with questions.
A couple of considerations when rolling data from lower to higher time grains:
- Daily data loses time zone querying as everything is rolled up to a single time zone (usually UTC)
- Consider hashed compaction when going from hourly to daily to reduce data size even further
- Watch out for rolling up metrics. Some metrics should be summed—but others (like a bid floor or campaign budget) should stay unique and be rolled up as a max
### Dimension Stripping
Dimension stripping is another tool to reduce data size by removing high cardinality fields that are not required for analysis. While this can be done upfront in the dataset, another practice would be to drop these fields at certain intervals when they no longer add business value. Most frequently, we see a couple of decision points where these fields are dropped:
- After a day to first week, dropping user level details no longer needed for monitoring
- After a week to multiple weeks, dropping "double click" level details that aren't needed for reporting (e.g., the minor release number on an Operating System field)
- After a month to months, dropping fields no longer interesting for analysis
### Sampling & Datasketches
There are times when you may look at sampling data feeds to trade data accuracy for lower costs and faster query speeds. Sampling involves sending only a percentage of your data, then extrapolating the values to get an estimate. Rill does not recommend sampling your primary KPIs, any records that require a join, or are tied to revenue. This filtered data should be decided in random fashion to not skew or bias the results. Please note, tracking uniques is not recommended if you choose to sample.
If looking to track uniques, but with smaller datasets and significantly improved performance, you can load unique values (IP addresses, user IDs, URLs, etc.) with [datasketches](https://datasketches.apache.org). There are multiple types of datasketches supported depending on your engine. At a high level, datasketches use algorithms to approximate unique values. Common use cases for datasketches include count distincts (campaign reach, unique visitors) and quantiles (time spent, frequency). Check out the [Apache Datasketches](https://datasketches.apache.org/docs/Architecture/MajorSketchFamilies.html) site for more details on methodology and use cases.
### Lookups
While joins can kill the performance of [OLAP engines](/developers/build/connectors/olap), lookups (key-value pairs) are common to reduce data size and improve query speeds. Lookups can be done during ingestion time (a static lookup to enrich the source data) or at query time (dynamic lookups).
**Static Lookups**
Static lookups are lookups that are ingested at processing time. When a record is being processed, if a match is found between the record and lookup's key, the lookup's corresponding value at that moment in time is extracted and carbon-copied into Druid for the records it processed.
Static lookups are best suited for:
- Dimensions with values that require a historical record for how they have changed over time
- Values that are never expected to change (leverage Dynamic Lookups if the values are expected to change)
- Extremely large lookups (hundreds of thousands of records or >50MB lookup file) to improve query performance
Customers typically store lookup values in S3 or GCS, and the lookup file is then updated by customers as needed and consumed by ETL logic.
**Dynamic Lookups**
Since static lookups transform and store the data permanently, any changes to the mapping would require reprocessing the entire dataset to ensure consistency. To address the case when values in a lookup are expected to change with time, we developed dynamic lookups. Dynamic lookups, also known as Query Time Lookups, are lookups that are retrieved at query time, as opposed to being used at ingestion time.
Benefits of dynamic lookups include:
- Historical continuity for dimensions that change frequently without reprocessing the entire dataset
- Time savings, because there is no dataset reprocessing required to complete the update
- Dynamic lookups are kept separate from the dataset. Thus, any human errors introduced in the lookup do not impact the underlying dataset
- Ability for users to create new dimension tables from metadata associated with a dimension table. For example, account ownership can change during the course of a quarter. In such cases, a dynamic lookup can be updated on the fly to reflect the most current changes
---
####### Pinot
---
title: Pinot
description: Power Rill dashboards using Pinot
sidebar_label: Pinot
sidebar_position: 20
---
[Apache Pinot](https://docs.pinot.apache.org/) is a real-time distributed OLAP datastore purpose-built for low-latency, high-throughput analytics, and is perfect for user-facing analytical workloads.
Rill supports connecting to an existing Pinot cluster via a "live connector" and using it as an OLAP engine built against [external tables](/developers/build/connectors/olap#external-olap-tables) to power Rill dashboards. This is particularly useful when working with extremely large datasets (hundreds of GBs or even TB+ in size).
## Configuring Rill Developer with Pinot
When using Rill for local development, there are a few options to configure Rill to enable Pinot as an OLAP engine:
1. Connect to an OLAP engine via Add Data. This will automatically create the `pinot.yaml` file in your `connectors` directory and populate the `.env` file with `PINOT_PASSWORD` or `PINOT_DSN` depending on which you select in the UI.
For more information on supported parameters, see our [Pinot connector YAML reference docs](/reference/project-files/connectors#pinot).
```yaml
type: connector
driver: pinot
dsn: "{{ .env.PINOT_DSN }}"
```
1. You can set `PINOT_DSN` in your project's `.env` file or try pulling existing credentials locally using `rill env pull` if the project has already been deployed to Rill Cloud.
:::tip Getting DSN errors in dashboards after setting `.env`?
If you are facing issues related to DSN connection errors in your dashboards even after setting the connection string via the project's `.env` file, try restarting Rill using the `rill start --reset` command.
:::
## Connection String (DSN)
Rill connects to Pinot using the [Pinot Golang Client](https://docs.pinot.apache.org/users/clients/golang) and requires a connection string of the following format: `http://:@:?controller=:`. If `user` or `password` contain special characters, they should be URL encoded (i.e., `p@ssword` -> `p%40ssword`). This should be set in the `PINOT_DSN` property in Rill.
As an example, this typically looks like:
```bash
PINOT_DSN="http(s)://username:password@localhost:8000?controller=localhost:9000"
```
:::info Need help connecting to Pinot?
If you would like to connect Rill to an existing Pinot instance, please don't hesitate to [contact us](/contact). We'd love to help!
:::
## Setting the Default OLAP Connection
You'll also need to update the `olap_connector` property in your project's `rill.yaml` to change the default OLAP engine to Pinot:
```yaml
olap_connector: pinot
```
:::info Interested in using multiple OLAP engines in the same project?
Please see our [Using Multiple OLAP Engines](/developers/build/connectors/olap/multiple-olap) page.
:::
## Configuring Rill Cloud
When deploying a Pinot-backed project to Rill Cloud, you have the following options to pass the appropriate connection string to Rill Cloud:
1. If you have followed the UI to create your Pinot connector, the password or DSN should already exist in the .env file. During the deployment process, this `.env` file is automatically pushed with the deployment.
2. If `PINOT_DSN` has already been set in your project `.env`, you can push and update these variables directly in your cloud deployment by using the `rill env push` command.
## Support for Multi-Valued Dimensions
Multi-valued dimensions need to be defined in the dashboard YAML as expressions using the `arrayToMv` function. For example, if `RandomAirports` is a multi-valued column in a Pinot table, then the dimension definition will look like:
```yaml
- display_name: RandomAirports
expression: arrayToMv(RandomAirports)
name: RandomAirports
description: "Random Airports"
```
Refer to the [Dashboard YAML](/reference/project-files/explore-dashboards) reference page for all dimension properties in detail.
:::note
Pinot does not support the unnest function, so don't set the `unnest` property to true in the dimension definition of the dashboard YAML.
:::
## Caching Query Results
To reduce broker load on dashboards with repeat traffic, see [Caching query results](/developers/build/metrics-view/underlying-model#caching-query-results) on live connectors.
## Additional Notes
- At the moment, we do not support modeling with Pinot. If this is something you're interested in, please [contact us](/contact).
- For dashboards powered by Pinot, [measure definitions](/developers/build/metrics-view/#measures) are required to follow [Pinot SQL](https://docs.pinot.apache.org/users/user-guide-query/querying-pinot) syntax.
---
####### Snowflake
---
title: Snowflake
description: Power Rill dashboards using Snowflake
sidebar_label: Snowflake
sidebar_position: 24
---
[Snowflake](https://docs.snowflake.com) is a cloud data platform known for its scalability, ease of use, and separation of storage and compute. Rill supports connecting to Snowflake as a live connector, allowing you to build metrics views and dashboards directly on top of existing Snowflake tables — no data movement required.
:::tip Snowflake as a Live Connector vs. Data Source Connector
Rill supports Snowflake in two distinct modes:
- **OLAP (Live Connector)** — Rill queries Snowflake directly at dashboard load time. No data is ingested into Rill. Use this when your data is already modeled and optimized in Snowflake and you want Rill as a visual layer on top. Set `olap_connector: snowflake` in `rill.yaml`.
- **Data Source Connector** — Rill extracts data from Snowflake and ingests it into its embedded engine (DuckDB or ClickHouse). Use this when you want Rill to manage the data pipeline, apply transformations via SQL models, or combine Snowflake data with other sources. See the [Snowflake data source docs](/developers/build/connectors/data-source/snowflake).
In general, use the live connector if your Snowflake tables are already production-ready and large. Use data source ingestion if you need to transform, join, or enrich the data before building dashboards.
:::
## Connect to Snowflake
After selecting "Add Data", select Snowflake and fill in your connection parameters. This will automatically create the `snowflake.yaml` file in your `connectors` directory and populate the `.env` file with your credentials.
For more information on supported parameters, see our [Snowflake connector YAML reference docs](/reference/project-files/connectors#snowflake).
```yaml
type: connector
driver: snowflake
dsn: "{{ .env.SNOWFLAKE_DSN }}"
```
:::tip Finding your account identifier
Your account identifier appears in your Snowflake URL — it's everything before `.snowflakecomputing.com`. For example, if your URL is `https://xy12345.us-east-1.snowflakecomputing.com`, your account identifier is `xy12345.us-east-1`.
:::
### Key-Pair Authentication (Recommended)
For production use, Snowflake recommends key-pair authentication over passwords. Generate an unencrypted PKCS#8 private key and base64-encode it:
```bash
# Generate a 2048-bit unencrypted PKCS#8 private key
openssl genrsa 2048 | openssl pkcs8 -topk8 -nocrypt -out rsa_key.p8
# Base64 URL-encode for use in Rill
base64 -w 0 rsa_key.p8
```
Then set the encoded key in your connector:
```yaml
type: connector
driver: snowflake
account: ""
user: ""
private_key: "{{ .env.SNOWFLAKE_PRIVATE_KEY }}"
```
See [Snowflake's key-pair authentication docs](https://docs.snowflake.com/en/guide/key-pair-auth) for full setup instructions.
## Build a Metrics View on Snowflake
Once connected, set Snowflake as the OLAP connector in your `rill.yaml`:
```yaml
olap_connector: snowflake
```
Then create a metrics view that references a Snowflake table:
```yaml
type: metrics_view
connector: snowflake
database: MY_DATABASE
database_schema: MY_SCHEMA
model: MY_TABLE
timeseries: created_at
dimensions:
- column: region
- column: category
measures:
- name: total_revenue
expression: SUM(revenue)
```
:::note
Rill supports metrics views directly on Snowflake as a live connector. Incremental models and partitioned ingestion are not supported in live connector mode.
:::
To reduce warehouse spend on dashboards with repeat traffic, see [Caching query results](/developers/build/metrics-view/underlying-model#caching-query-results) on live connectors.
---
####### StarRocks
---
title: StarRocks
description: Power Rill dashboards using StarRocks
sidebar_label: StarRocks
sidebar_position: 25
---
[StarRocks](https://www.starrocks.io/) is an open-source, high-performance analytical database designed for real-time, multi-dimensional analytics on large-scale data. It supports both primary key and aggregate data models, making it suitable for a variety of analytical workloads including real-time dashboards, ad-hoc queries, and complex analytical tasks.
:::note Supported Versions
Rill supports connecting to StarRocks 4.0 or newer versions.
:::
:::info
Rill supports connecting to an existing StarRocks cluster via a read-only OLAP connector and using it to power Rill dashboards with [external tables](/developers/build/connectors/olap#external-olap-tables).
:::
## Connect to StarRocks
When using StarRocks for local development, you can connect via connection parameters or by using a DSN.
After selecting "Add Data", select StarRocks and fill in your connection parameters. This will automatically create the `starrocks.yaml` file in your `connectors` directory and populate the `.env` file with `STARROCKS_PASSWORD`.
### Connection Parameters
```yaml
type: connector
driver: starrocks
host:
port: 9030
username:
password: "{{ .env.STARROCKS_PASSWORD }}"
catalog: default_catalog
database:
ssl: false
```
### Connection String (DSN)
Rill can also connect to StarRocks using a DSN connection string. StarRocks uses MySQL protocol, so the connection string must follow the MySQL DSN format:
```yaml
type: connector
driver: starrocks
dsn: "{{ .env.STARROCKS_DSN }}"
```
#### Using default_catalog
For `default_catalog`, you can specify database directly in the DSN path (MySQL-style):
```
user:password@tcp(host:9030)/my_database?parseTime=true
```
#### Using external catalogs with DSN
For external catalogs (Iceberg, Hive, etc.), set `catalog` and `database` as separate properties (do not include database in DSN):
```yaml
type: connector
driver: starrocks
dsn: "user:password@tcp(host:9030)/?parseTime=true"
catalog: iceberg_catalog
database: my_database
```
If `catalog` is not specified, it defaults to `default_catalog`.
:::warning DSN Format
Only MySQL-style DSN format is supported. The `starrocks://` URL scheme is **not** supported. When using DSN, do not set `host`, `port`, `username`, `password` separately — these must be included in the DSN string.
:::
## Configuration Properties
| Property | Description | Default |
| ------------- | --------------------------------------------------------------------- | -------------------- |
| `host` | StarRocks FE (Frontend) server hostname | Required (if no DSN) |
| `port` | MySQL protocol port of StarRocks FE | `9030` |
| `username` | Username for authentication | `root` |
| `password` | Password for authentication | - |
| `catalog` | StarRocks catalog name (for external catalogs like Iceberg, Hive) | `default_catalog` |
| `database` | StarRocks database name | - |
| `ssl` | Enable SSL/TLS encryption | `false` |
| `dsn` | MySQL-format connection string (alternative to individual parameters) | - |
| `log_queries` | Enable logging of all SQL queries (useful for debugging) | `false` |
## External Catalogs
StarRocks supports external catalogs for querying data in Hive, Iceberg, Delta Lake, and other external data sources. To use an external catalog:
1. Set the `catalog` property to your external catalog name (e.g., `iceberg_catalog`)
2. Set the `database` property to the database within that catalog
```yaml
type: connector
driver: starrocks
host: starrocks-fe.example.com
port: 9030
username: analyst
password: "{{ .env.STARROCKS_PASSWORD }}"
catalog: iceberg_catalog
database: my_database
```
## Naming Mapping
StarRocks uses a three-level hierarchy: Catalog > Database > Table. In Rill's API:
| Rill Parameter | StarRocks Concept | Example |
| ---------------- | ----------------- | ------------------------------------ |
| `database` | Catalog | `default_catalog`, `iceberg_catalog` |
| `databaseSchema` | Database | `my_database` |
| `table` | Table | `my_table` |
## Creating Metrics Views
When creating metrics views against StarRocks tables, use the `table` property with `database_schema` to reference your data:
```yaml
type: metrics_view
display_name: My Dashboard
table: my_table
database_schema: my_database
timeseries: timestamp
dimensions:
- name: category
column: category
measures:
- name: total_count
expression: COUNT(*)
```
## Troubleshooting
### Connection Issues
If you encounter connection issues:
1. Verify the FE node hostname and port (default: 9030)
2. Check that your user has appropriate permissions
3. Ensure network connectivity to the StarRocks FE node
4. For SSL connections, verify SSL is enabled on the StarRocks server
### Timezone Handling
All timestamp values are returned in UTC. The driver parses DATETIME values from StarRocks as UTC time.
## Known Limitations
- **Read-only connector**: StarRocks is a read-only OLAP connector. Model creation and execution is not supported.
:::info Need help connecting to StarRocks?
If you would like to connect Rill to an existing StarRocks instance, please don't hesitate to [contact us](/contact). We'd love to help!
:::
---
##### Services
---
####### Claude
---
title: Claude
description: Use Claude for AI features with your own API key
sidebar_label: Claude
sidebar_position: 10
---
[Claude](https://www.anthropic.com/claude) is Anthropic's AI assistant, designed to be helpful, harmless, and honest. Rill supports connecting to Claude using your own API key and configuration parameters to enable AI-powered conversations and data analysis features.
## API Key
Rill will use your configured Claude connector if available, or fall back to its built-in LLM service if no custom configuration is provided. Once configured, your API key will be used for AI features in both Rill Developer and Rill Cloud, including the AI Agent builder. You can configure your API key in your project's `.env` file and reference the [credentials in a connector YAML](/reference/project-files/connectors#claude).
### Claude API Key
To configure Claude access, you'll need to obtain an API key and configure it in your project.
1. **Obtain your Claude API key** from the [Anthropic Console](https://console.anthropic.com/settings/keys).
2. **Create the connector YAML:**
Create `connectors/claude.yaml` in your project:
```yaml
type: connector
driver: claude
api_key: "{{ .env.CLAUDE_API_KEY }}"
```
3. **Set up environment variable:**
If configuring manually, ensure your project's `.env` file contains the key before starting Rill:
```env
claude_api_key=sk-ant-...
```
4. **Configure Claude as the default AI connector:**
Add the following to your `rill.yaml` to use Claude as the AI provider for your project:
```yaml
ai_connector: claude
```
For details on managing credentials across environments, see [Configure Local Credentials](/developers/build/connectors/credentials).
## Configuration Options
For additional configuration options (model, temperature, token limits, etc.), see the [Claude connector reference](/reference/project-files/connectors#claude).
## Deploy to Rill Cloud
Rill requires you to explicitly provide a Claude API key to use the Claude connector. See the [connector reference](/reference/project-files/connectors#claude) for details.
For details on pushing and pulling credentials between environments, see [Configure Local Credentials](/developers/build/connectors/credentials#rill-env-push).
---
####### Gemini
---
title: Gemini
description: Use Gemini for AI features with your own API key
sidebar_label: Gemini
sidebar_position: 20
---
[Gemini](https://ai.google.dev/) is Google's family of AI models, designed for a wide range of reasoning, coding, and multimodal tasks. Rill supports connecting to Gemini using your own API key and configuration parameters to enable AI-powered conversations and data analysis features.
## API Key
Rill will use your configured Gemini connector if available, or fall back to its built-in LLM service if no custom configuration is provided. Once configured, your API key will be used for AI features in both Rill Developer and Rill Cloud, including the AI Agent builder. You can configure your API key in your project's `.env` file and reference the [credentials in a connector YAML](/reference/project-files/connectors#gemini).
### Gemini API Key
To configure Gemini access, you'll need to obtain an API key and configure it in your project.
1. **Obtain your Gemini API key** from [Google AI Studio](https://aistudio.google.com/apikey).
2. **Create the connector YAML:**
Create `connectors/gemini.yaml` in your project:
```yaml
type: connector
driver: gemini
api_key: "{{ .env.gemini_api_key }}"
```
3. **Set up environment variable:**
If configuring manually, ensure your project's `.env` file contains the key before starting Rill:
```env
gemini_api_key=AI...
```
4. **Configure Gemini as the default AI connector:**
Add the following to your `rill.yaml` to use Gemini as the AI provider for your project:
```yaml
ai_connector: gemini
```
For details on managing credentials across environments, see [Configure Local Credentials](/developers/build/connectors/credentials).
## Configuration Options
For additional configuration options (model, temperature, token limits, etc.), see the [Gemini connector reference](/reference/project-files/connectors#gemini).
## Deploy to Rill Cloud
Rill requires you to explicitly provide a Gemini API key to use the Gemini connector. See the [connector reference](/reference/project-files/connectors#gemini) for details.
For details on pushing and pulling credentials between environments, see [Configure Local Credentials](/developers/build/connectors/credentials#rill-env-push).
---
####### "Service Integrations"
---
title: "Service Integrations"
description: Connect Rill to external services like OpenAI and Slack
sidebar_position: 7
---
import ConnectorIcon from '@site/src/components/ConnectorIcon';
Service Integrations extend Rill's capabilities by connecting to third-party services. Unlike data connectors that import data into Rill, these integrations enable features like AI-powered analytics and notifications.
## AI Integrations
### Claude
### Gemini
### OpenAI
}
header="AI"
content="Create and define a Claude Connector with your own API key."
link="/developers/build/connectors/services/claude"
linkLabel="Learn more"
referenceLink="claude"
/>
}
header="Gemini"
content="Create and define a Gemini Connector with your own API key."
link="/developers/build/connectors/services/gemini"
linkLabel="Learn more"
referenceLink="gemini"
/>
}
header="AI"
content="Create and define an OpenAI Connector with your own API key."
link="/developers/build/connectors/services/openai"
linkLabel="Learn more"
referenceLink="openai"
/>
## Notifications
### Slack
}
header="Slack"
content="Connect to Slack to send alerts and messages from Rill."
link="/developers/build/connectors/services/slack"
linkLabel="Learn more"
referenceLink="slack"
/>
---
####### OpenAI
---
title: OpenAI
description: Connect to OpenAI services for AI-powered features
sidebar_label: OpenAI
sidebar_position: 30
---
[OpenAI](https://openai.com/) provides powerful AI services including language models, embeddings, and other machine learning capabilities. Rill supports connecting to OpenAI services using your own API key and configuration parameters to enable AI-powered conversations and data analysis features.
## API Key
Rill will use your configured OpenAI connector if available, or fall back to its built-in LLM service if no custom configuration is provided. Once configured, your API key will be used for AI features in both Rill Developer and Rill Cloud, including the AI Agent builder. You can configure your API key in your project's `.env` file and reference the [credentials in a connector YAML](/reference/project-files/connectors#openai).
### OpenAI API Key
To configure OpenAI access, you'll need to obtain an API key and configure it in your project.
1. **Obtain your OpenAI API key** from the [OpenAI Platform](https://platform.openai.com/api-keys).
2. **Create the connector YAML:**
Create `connectors/openai.yaml` in your project:
```yaml
type: connector
driver: openai
api_key: "{{ .env.OPENAI_API_KEY }}"
```
3. **Set up environment variable:**
If configuring manually, ensure your project's `.env` file contains the key before starting Rill:
```env
OPENAI_API_KEY=sk-...
```
4. **Configure OpenAI as the default AI connector:**
Add the following to your `rill.yaml` to use OpenAI as the AI provider for your project:
```yaml
ai_connector: openai
```
For details on managing credentials across environments, see [Configure Local Credentials](/developers/build/connectors/credentials).
## Configuration Options
For additional configuration options (model, base URL, API type, etc.), see the [OpenAI connector reference](/reference/project-files/connectors#openai).
## Deploy to Rill Cloud
Rill requires you to explicitly provide an OpenAI API key to use the OpenAI connector. See the [connector reference](/reference/project-files/connectors#openai) for details.
For details on pushing and pulling credentials between environments, see [Configure Local Credentials](/developers/build/connectors/credentials#rill-env-push).
---
####### Slack Integration
---
title: Slack Integration
description: Connect to Slack for data extraction and analytics
sidebar_label: Slack
sidebar_position: 40
---
## Overview
[Slack](https://slack.com/) is a popular messaging platform that allows teams to communicate and collaborate in real-time.
Rill supports sending notifications to Slack channels using the [Slack API](https://api.slack.com/).
This can be useful for sending alerts and reports to your team members.
## Setting up the Slack integration
Rill Cloud can send alert notifications to channels and/or as direct messages. This will require a Slack application to first be created and configured in your workspace with the necessary [permission scopes](https://api.slack.com/scopes). To set up your Slack application, follow the steps provided in the [Slack documentation](https://api.slack.com/start/quickstart) and configure your app within the appropriate permissions depending on the notification type that you wish to use (see below).
### Slack channels
Sending notifications to a specific channel (public or private) requires the [`chat:write`](https://api.slack.com/scopes/chat:write) scope.
:::info
The application will also need to be added to the channel for the notification to be sent.
:::
### Direct messages
Sending notifications via a direct message requires the [`chat:write`](https://api.slack.com/scopes/chat:write), [`users:read`](https://api.slack.com/scopes/users:read), and [`users:read.email`](https://api.slack.com/scopes/users:read.email) scopes.
:::tip
The last two scopes are required to find the user's ID by email.
:::
## Enabling the Slack integration in your project
Once the Slack integration has been set up, the Slack destination will need to be enabled on a per project basis (note - alerts can only be configured on projects deployed to Rill Cloud). This requires the `SLACK_BOT_TOKEN` connector variable to be set, which can be configured in Rill in a manner very similar to [setting credentials](/developers/deploy/deploy-credentials) for other connectors. Please use one of the available options below.
### Creating a Slack.yaml connector
Please refer to our [connector YAML reference documentation](/reference/project-files/connectors#slack) for more details.
### Updating the `.env` file directly
Within your project's `.env` file (i.e. `/.env`), you can set this connector variable with the Slack Bot User OAuth Token:
```shell
SLACK_BOT_TOKEN=
```
Afterwards, if the project has already been deployed to Rill Cloud, you can `rill env push` to update your cloud deployment accordingly.
### Using the `rill env set` command
Another option to set this connector variable within your project is to use the `rill env set` command, i.e.:
```shell
rill env set SLACK_BOT_TOKEN
```
Afterwards, if the project has already been deployed to Rill Cloud, you can `rill env push` to update your cloud deployment accordingly.
### Enabling the Slack connector through `rill.yaml`
You can enable the Slack "connector" within your project by updating your project's `rill.yaml` file with the following configuration:
```yaml
# Rest of your rill.yaml contents
connectors:
- name: slack
type: slack
```
## Deploy to Rill Cloud
When deploying a project to Rill Cloud, Rill requires you to explicitly provide Slack credentials used in your project. Please refer to our [connector YAML reference docs](/reference/project-files/connectors#slack) for more information.
If you subsequently add sources that require new credentials (or if you simply entered the wrong credentials during the initial deploy), you can update the credentials by pushing the `Deploy` button to update your project or by running the following command in the CLI:
```
rill env push
```
---
###### "Connect to your Data"
---
title: "Connect to your Data"
description: Import local files or remote data sources
sidebar_label: "Connectors"
sidebar_position: 0
toc_max_heading_level: 3
className: connect-connect
---
import ConnectorIcon from '@site/src/components/ConnectorIcon';
## Connection Strategies
Rill offers flexible connection strategies to fit different data architectures and requirements.
- ### _[Rill Managed OLAP + Data Ingestion (Default)](/developers/build/connectors/data-source)_:
Use Rill's embedded **ClickHouse / DuckDB** (depending on size of data) as the OLAP engine and ingest data from external sources. Full Rill functionality is available with [some caveats](/developers/build/connectors/data-source#managed-olap-engine-caveats) depending on which embedded engine you select.
:::tip Rill Defaults with DuckDB
When starting Rill for the first time, Rill will auto-populate the connector with a `duckdb.yaml`. To use ClickHouse, create a managed ClickHouse connector by selecting "Add Data", then ClickHouse, and finally "Rill-managed ClickHouse" in the UI. For more information, see [Rill Managed ClickHouse](/developers/build/connectors/olap/clickhouse#rill-managed-clickhouse).
:::
- ### _[Bring Your Own OLAP (BYO OLAP)](/developers/build/connectors/olap)_:
For large-scale datasets (100GB+) or existing [OLAP infrastructure](/developers/build/connectors/olap#what-is-olap), connect to existing **ClickHouse**, **Druid**, **Pinot**, or **MotherDuck** instances. Use Rill's "live connectors" to ingest data directly into your OLAP engines.
## OLAP Engines
### ClickHouse
### Databricks
### Druid
### DuckDB
### DuckLake
### Google BigQuery
### MotherDuck
### Pinot
### Snowflake
### StarRocks
}
content="High-performance columnar database for real-time analytics and data warehousing."
link="/developers/build/connectors/olap/clickhouse"
linkLabel="Learn more"
referenceLink="clickhouse"
/>
}
header="Databricks"
content="Lakehouse platform with native support for metrics views as a live connector."
link="/developers/build/connectors/olap/databricks"
linkLabel="Learn more"
referenceLink="databricks"
/>
}
content="Real-time analytics database designed for high-performance OLAP queries."
link="/developers/build/connectors/olap/druid"
linkLabel="Learn more"
referenceLink="druid"
/>
}
content="DuckDB is the default engine for Rill Developer."
link="/developers/build/connectors/olap/duckdb"
linkLabel="Learn more"
referenceLink="duckdb"
/>
}
content="Open lakehouse format built on DuckDB — catalog in SQL, data in Parquet."
link="/developers/build/connectors/olap/ducklake"
linkLabel="Learn more"
referenceLink="ducklake"
/>
}
content="Connect to Google BigQuery for analytics and data warehousing with service account authentication."
link="/developers/build/connectors/olap/bigquery"
linkLabel="Learn more"
referenceLink="bigquery"
/>
}
content="Cloud-native DuckDB service for scalable analytics and data processing."
link="/developers/build/connectors/olap/motherduck"
linkLabel="Learn more"
referenceLink="motherduck"
/>
}
content="Distributed OLAP datastore for real-time analytics and business intelligence."
link="/developers/build/connectors/olap/pinot"
linkLabel="Learn more"
referenceLink="pinot"
/>
}
header="Snowflake"
content="Cloud data warehouse with native support for metrics views as a live connector."
link="/developers/build/connectors/olap/snowflake"
linkLabel="Learn more"
referenceLink="snowflake"
/>
}
content="Distributed OLAP datastore for real-time analytics and business intelligence."
link="/developers/build/connectors/olap/starrocks"
linkLabel="Learn more"
referenceLink="starrocks"
/>
:::tip Missing an OLAP Engine?
Rill is continually evaluating additional OLAP engines that can be added. For a full list of OLAP engines that we support, refer to our [OLAP Engines](/developers/build/connectors/olap) page. If you don't see an OLAP engine that you'd like to use, please don't hesitate to [reach out](/contact)!
:::
## Data Warehouses
### Athena
### BigQuery
### Databricks
### Redshift
### Snowflake
}
header="Athena"
content="Connect to Amazon Athena for serverless querying of data stored in S3 using standard SQL."
link="/developers/build/connectors/data-source/athena"
linkLabel="Learn more"
referenceLink="athena"
/>
}
header="BigQuery"
content="Connect to Google BigQuery for analytics and data warehousing with service account authentication."
link="/developers/build/connectors/data-source/bigquery"
linkLabel="Learn more"
referenceLink="bigquery"
/>
}
header="Databricks"
content="Connect to Databricks SQL warehouses with personal access token authentication."
link="/developers/build/connectors/data-source/databricks"
linkLabel="Learn more"
referenceLink="databricks"
/>
}
header="Redshift"
content="Connect to Amazon Redshift data warehouse with AWS credentials and support for both provisioned and serverless clusters."
link="/developers/build/connectors/data-source/redshift"
linkLabel="Learn more"
referenceLink="redshift"
/>
}
header="Snowflake"
content="Connect to Snowflake data warehouse with support for individual credentials and JWT authentication."
link="/developers/build/connectors/data-source/snowflake"
linkLabel="Learn more"
referenceLink="snowflake"
/>
## Databases
### MySQL
### PostgreSQL
### SQLite
### Supabase
}
header="MySQL"
content="Connect to MySQL databases with support for various authentication methods and SSL connections."
link="/developers/build/connectors/data-source/mysql"
linkLabel="Learn more"
referenceLink="mysql"
/>
}
header="PostgreSQL"
content="Connect to PostgreSQL databases with support for SSL connections and various authentication methods."
link="/developers/build/connectors/data-source/postgres"
linkLabel="Learn more"
referenceLink="postgresql"
/>
}
header="SQLite"
content="Connect to SQLite databases for lightweight, file-based data storage and querying."
link="/developers/build/connectors/data-source/sqlite"
linkLabel="Learn more"
/>
}
header="Supabase"
content="Connect to Supabase's managed PostgreSQL databases with SSL support and standard connection methods."
link="/developers/build/connectors/data-source/supabase"
linkLabel="Learn more"
referenceLink="supabase"
/>
## Object Storage
### Amazon S3
### Google Cloud Storage
### Microsoft Azure Blob Storage
}
header="Amazon S3"
content="Connect to Amazon S3 buckets to read data files including CSV, JSON, Parquet, and compressed formats."
link="/developers/build/connectors/data-source/s3"
linkLabel="Learn more"
referenceLink="s3"
/>
}
header="Google Cloud Storage"
content="Google Cloud Storage provides scalable object storage and data lakes."
link="/developers/build/connectors/data-source/gcs"
linkLabel="Learn more"
referenceLink="gcs"
/>
}
header="Azure"
content="Connect to Microsoft Azure Blob Storage to read data files with support for various formats."
link="/developers/build/connectors/data-source/azure"
linkLabel="Learn more"
referenceLink="azure"
/>
## Table Formats
### Delta Lake
### Apache Iceberg
}
header="Delta Lake"
content="Read Delta tables directly from object storage through compatible query engines."
link="/developers/build/connectors/data-source/delta"
linkLabel="Learn more"
/>
}
header="Apache Iceberg"
content="Read Iceberg tables directly from object storage through compatible query engines."
link="/developers/build/connectors/data-source/iceberg"
linkLabel="Learn more"
/>
## Other Data Connectors
### External DuckDB
### Google Sheets
### HTTPS
### Local File
### Salesforce
}
header="DuckDB"
content="Attach your local DuckDB database to Rill's embedded database."
link="/developers/build/connectors/data-source/duckdb"
linkLabel="Learn more"
referenceLink="external-duckdb"
/>
}
header="Google Sheets"
content="Connect to public Google Sheets to read data from spreadsheets with support for multiple sheets."
link="/developers/build/connectors/data-source/googlesheets"
linkLabel="Learn more"
/>
https:// }
header="HTTPS"
content="Download data from HTTP/HTTPS URLs with support for various authentication methods."
link="/developers/build/connectors/data-source/https"
linkLabel="Learn more"
referenceLink="https"
/>
}
header="Local File"
content="Read data from local files including CSV, JSON, Parquet, and compressed formats."
link="/developers/build/connectors/data-source/local-file"
linkLabel="Learn more"
/>
}
header="Salesforce"
content="Connect to Salesforce to extract data from objects and queries using the Salesforce API."
link="/developers/build/connectors/data-source/salesforce"
linkLabel="Learn more"
/>
:::tip Missing a connector?
We're constantly adding new data connectors. If you don't see what you need, [let us know](/contact) and we'll help you get connected.
:::
## Service Integrations
### Claude
### Gemini
### OpenAI
### Slack
}
header="AI"
content="Create and define a Claude Connector with your own API key."
link="/developers/build/connectors/services/claude"
linkLabel="Learn more"
referenceLink="claude"
/>
}
header="Gemini"
content="Create and define a Gemini Connector with your own API key."
link="/developers/build/connectors/services/gemini"
linkLabel="Learn more"
referenceLink="gemini"
/>
}
header="AI"
content="Create and define an OpenAI Connector with your own API key."
link="/developers/build/connectors/services/openai"
linkLabel="Learn more"
referenceLink="openai"
/>
}
header="Slack"
content="Connect to Slack to send alerts and messages from Rill."
link="/developers/build/connectors/services/slack"
linkLabel="Learn more"
referenceLink="slack"
/>
---
###### Configure Local Credentials
---
title: Configure Local Credentials
sidebar_label: Configure Local Credentials
sidebar_position: 15
---
Rill requires credentials to connect to remote data sources such as private buckets (S3, GCS, Azure), data warehouses (Snowflake, BigQuery), OLAP engines (ClickHouse, Apache Druid), or other DuckDB sources (MotherDuck). Please refer to the appropriate [connector](/developers/build/connectors) and [OLAP engine](/developers/build/connectors/olap) page for instructions to configure credentials accordingly.
At a high level, configuring credentials and credential management in Rill can be broken down into three categories:
- Setting credentials for Rill Developer
- [Setting credentials for a Rill Cloud project](/developers/deploy/deploy-credentials)
- [Pushing and pulling credentials to / from Rill Cloud]( /guide/administration/project-settings/variables-and-credentials)
## Setting credentials for Rill Developer
When reading from a data source (or using a different OLAP engine), Rill will attempt to use credentials in the following order of priority:
:::warning **Highly Recommended: Use .env for credentials**
While Rill **can** infer credentials from your local environment (AWS CLI, Azure CLI, Google Cloud CLI), **we HIGHLY recommend explicitly configuring credentials in your `.env` file** for better security, reliability, and portability. Environment-inferred credentials may vary across different setups and may not work consistently across different environments, team members, or when deploying to Rill Cloud.
:::
1. **Credentials referenced in connection strings or DSN within YAML files (RECOMMENDED)** - The UI creates YAML configurations that reference credentials from your `.env` file using templating (see [Connector YAML](/reference/project-files/connectors) for more details)
2. **Credentials passed in as variables** - When starting Rill Developer via `rill start --env key=value` (see [passing environment variables](/developers/build/connectors/templating) for more details)
3. **Credentials configured via CLI** - For [AWS](/developers/build/connectors/data-source/s3#method-4-local-aws-credentials-local-development-only) / [Azure](/developers/build/connectors/data-source/azure#method-5-azure-cli-authentication-local-development-only) / [Google Cloud](/developers/build/connectors/data-source/gcs#method-4-local-google-cloud-cli-credentials) - **NOT RECOMMENDED for production use**
For more details, please refer to the corresponding [connector](/developers/build/connectors) or [OLAP engine](/developers/build/connectors/olap) page.
:::note Ensuring security of credentials in use
If you plan to deploy a project (to Rill Cloud), it is not recommended to pass in credentials directly through the local connection string or DSN as your credentials will then be checked in directly to your Git repository (and thus accessible by others). To ensure better security, credentials should be passed in as a variable, configured locally, or specified in the project's local `.env` file (which is part of `.gitignore` and thus won't be included).
:::
## Variables
Project variables work exactly the same way as credentials and can be defined when starting rill via `--env key=value`, set in the `.env` file in the project directory, or defined in the rill.yaml.
### Passing Environment Variables
Environment variables in your local shell are not automatically passed to Rill Developer. You can pass these to Rill Developer using the `--env` flag. This is useful for referencing existing environment variables without exposing their values in your command history or configuration files.
For example, to use the `AWS_ACCESS_KEY_ID` environment variable:
```bash
rill start --env AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID
```
This will pull in the value of AWS_ACCESS_KEY_ID from your environment and make it available to your Rill project without exposing the credential value in your command or .env file. In Rill, you would reference it using: `"{{ .env.AWS_ACCESS_KEY_ID }}"`.
:::warning Development Only
Passing environment variables via rill start --env is intended for local development purposes only. These credentials are not automatically passed to Rill Cloud when you deploy your project. For production deployments, you must configure credentials separately in Rill Cloud using the project settings or rill env push.
:::
### What is a `.env` file?
A `.env` file is a plain text file that stores environment variables and credentials for your Rill project. It's located in your project's root directory and follows a simple `key=value` format.
The `.env` file serves several important purposes:
- **Security**: Keeps sensitive credentials out of your codebase and Git repository (`.env` files are automatically ignored by `.gitignore`)
- **Consistency**: Provides a standardized way to manage credentials across different environments (local development, staging, production)
- **Integration**: Works seamlessly with Rill's templating system, allowing YAML files to reference credentials using `"{{ .env.VARIABLE_NAME }}"` syntax
Example `.env` file:
```bash
# AWS S3 credentials
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# Google Cloud credentials
GOOGLE_APPLICATION_CREDENTIALS={"type":"service_account","project_id":"my-project"}
# Database connections
POSTGRES_PASSWORD=mypassword
SNOWFLAKE_PASSWORD=mysnowflakepassword
# Custom variables
my_custom_variable=some_value
```
When creating any connector in Rill via the UI, these will be **automatically generated** in the `.env` file.
Additional variables can then be usable and referenceable for [templating](/developers/build/connectors/templating) purposes in the local instance of your project.
### Credentials Naming Schema
When you create a connector through Rill's UI, credentials are automatically saved to your `.env` file using a standardized naming convention:
#### Generic Credentials (Shared Across Connectors)
Common cloud provider credentials use standard names without a driver prefix:
| Property | Environment Variable |
|----------|---------------------|
| Google Application Credentials | `GOOGLE_APPLICATION_CREDENTIALS` |
| AWS Access Key ID | `AWS_ACCESS_KEY_ID` |
| AWS Secret Access Key | `AWS_SECRET_ACCESS_KEY` |
| Azure Storage Connection String | `AZURE_STORAGE_CONNECTION_STRING` |
| Azure Storage Key | `AZURE_STORAGE_KEY` |
| Azure Storage SAS Token | `AZURE_STORAGE_SAS_TOKEN` |
| Snowflake Private Key | `PRIVATE_KEY` |
#### Driver-Specific Credentials
Credentials specific to a database driver use the `DRIVER_PROPERTY` format:
| Driver | Property | Environment Variable |
|--------|----------|---------------------|
| PostgreSQL | password | `POSTGRES_PASSWORD` |
| PostgreSQL | dsn | `POSTGRES_DSN` |
| MySQL | password | `MYSQL_PASSWORD` |
| Snowflake | password | `SNOWFLAKE_PASSWORD` |
| ClickHouse | password | `CLICKHOUSE_PASSWORD` |
#### Handling Multiple Connectors
When you create multiple connectors that use the same credential type, Rill automatically appends a numeric suffix to avoid conflicts:
```bash
# First BigQuery connector
GOOGLE_APPLICATION_CREDENTIALS={"type":"service_account",...}
# Second BigQuery connector
GOOGLE_APPLICATION_CREDENTIALS_1={"type":"service_account",...}
# Third BigQuery connector
GOOGLE_APPLICATION_CREDENTIALS_2={"type":"service_account",...}
```
This ensures each connector can reference its own credentials without overwriting existing ones.
#### Referencing Variables in YAML
Use the `{{ .env.VARIABLE_NAME }}` syntax to reference environment variables in your connector YAML files:
```yaml
google_application_credentials: "{{ .env.GOOGLE_APPLICATION_CREDENTIALS }}"
password: "{{ .env.POSTGRES_PASSWORD }}"
aws_access_key_id: "{{ .env.AWS_ACCESS_KEY_ID }}"
```
#### Case-Insensitive Variable Lookups
The `{{ env "VAR_NAME" }}` function (note: not `{{ .env.VAR_NAME }}`) provides case-insensitive variable lookups, which can be useful when variable names may have inconsistent casing:
```yaml
# All of these will match POSTGRES_PASSWORD in your .env file:
password: '{{ env "POSTGRES_PASSWORD" }}'
password: '{{ env "postgres_password" }}'
password: '{{ env "Postgres_Password" }}'
```
Note that `{{ .env.VAR_NAME }}` is **case-sensitive** — the variable name must match exactly.
:::note Legacy Naming Convention
Older projects may use the `connector..` syntax (e.g., `connector.druid.dsn`, `connector.clickhouse.dsn`). This format is still supported for backwards compatibility.
:::
:::tip Avoid committing sensitive information to Git
It's never a good idea to commit sensitive information to Git and it goes against security best practices. Similar to credentials, if there are sensitive variables that you don't want to commit publicly to your `rill.yaml` configuration file (and thus potentially accessible by others), it's recommended to set them in your `.env` file directly and/or use `rill env set` via the CLI (and then optionally push / pull them as necessary).
:::
## Cloning an Existing Project from Rill Cloud
If you cloned the project using `rill project clone ` and are an admin of that project, the credentials will be pulled automatically. Note that there are some limitations with monorepos where credentials may not be pulled correctly. In those cases, credentials are also pulled when running `rill start`, assuming you have already authenticated via the CLI with `rill login`.
For a detailed guide, see our [clone a project guide](/developers/tutorials/clone-a-project).
## Pulling Credentials and Variables from a Deployed Project on Rill Cloud
If you are making changes to an already deployed instance from Rill Cloud, it is possible to **pull** the credentials and variables from the Rill Cloud to your local instance of Rill Developer. If you've made any changes to the credentials, don't forget to run `rill env push` to push the variable changes to the project, or manually change these in the project's settings page.
### rill env pull
For projects that have been deployed to Rill Cloud, an added benefit of our Rill Developer-Cloud architecture is that credentials that have been configured can be pulled locally for easier reuse (instead of having to manually reconfigure these credentials in Rill Developer). To do this, you can run `rill env pull` from your project's root directory to retrieve the latest credentials (after cloning the project's git repository to your local environment).

:::info Overriding local credentials
Please note when you run `rill env pull`, Rill will *automatically override any existing credentials or variables* that have been configured in your project's `.env` file if there is a match in the key name. This may result in unexpected behavior if you are using different credentials locally.
:::
### rill env push
As a project admin, you can use `rill env push` to push your credentials to your Rill Cloud project.
- Rill Cloud will use the specified credentials and variables in this `.env` file for the deployed project.
- Other users will also be able to use `rill env pull` to retrieve these defined credentials for local use (with Rill Developer).
:::warning Overriding Cloud credentials
If a credential and/or variable has already been configured in Rill Cloud, Rill will warn you about overriding if you attempt to push a new value in your `.env` file. This is because overriding credentials can impact your deployed project and/or other users (if they pull these credentials locally).

:::
---
###### Development/Production Connector Environments
---
title: Development/Production Connector Environments
description: Development and Production Setup
sidebar_label: Development/Production Connectors
sidebar_position: 19
---
Templating serves several important purposes in your data workflow:
- **Cost Management**: Avoid running expensive queries on production data during development
- **Environment Separation**: Keep development and production data sources completely separate
- **Testing Safety**: Test your models and transformations without affecting production data
:::note Rill Developer defaults to 'dev'
Unless explicitly defined, Rill Developer will use a `dev` environment. If you want to emulate production locally, you can do so via `rill start --environment prod`.
:::
## Setting Up Environment Variables
You can set up environment variables in several locations in Rill. Please review our [configure local credentials documentation](/developers/build/connectors/credentials#setting-credentials-for-rill-developer) for more information.
## Referencing Environment Variables
Reference environment variables in your YAML files using the `{{ .env.VARIABLE_NAME }}` syntax:
```yaml
password: "{{ .env.POSTGRES_PASSWORD }}"
google_application_credentials: "{{ .env.GOOGLE_APPLICATION_CREDENTIALS }}"
aws_access_key_id: "{{ .env.AWS_ACCESS_KEY_ID }}"
```
:::tip Case-Insensitive Lookups
The `'{{ env "VAR_NAME" }}'` function provides case-insensitive variable lookups, so `'{{ env "my_var" }}'` will match `MY_VAR` in your `.env` file.
**Note:** If your `.env` file contains multiple variables that differ only by case (e.g., both `my_var` and `MY_VAR`), the behavior is undefined. Avoid defining variables with the same name in different cases.
:::
## Environment-Specific Connectors
The most common use case for connector templating is defining separate databases for your development and production operations. This approach gives you the freedom to experiment, test, and iterate on your models without the risk of accidentally modifying or corrupting your production data.
### Example: ClickHouse Connector with Environment Separation
Here's how you can configure a ClickHouse connector to use different environments:
```yaml
type: connector
driver: clickhouse
dev:
dsn: "clickhouse://user:password@localhost:9000/dev_database" # ClickHouse connection DSN
# Production environment configuration
prod:
host: "{{ .env.CLICKHOUSE_HOST }}"
port: "{{ .env.CLICKHOUSE_PORT }}"
database: "{{ .env.CLICKHOUSE_DATABASE }}"
username: "{{ .env.CLICKHOUSE_USERNAME }}"
password: "{{ .env.CLICKHOUSE_PASSWORD }}"
ssl: true
cluster: "{{ .env.CLICKHOUSE_CLUSTER }}"
```
In this example:
- **Development**: Uses a managed ClickHouse instance that Rill starts locally
- **Production**: Connects to your existing ClickHouse cluster using environment variables for secure configuration
:::warning Managing development and production credentials
Some connectors will reference two unique databases and require two unique credentials for development and production. In this case, you can either define a unique environmental variable for both and reference them separately in the connector.
For example:
```yaml
"{{ .env.DEV_CLICKHOUSE_USERNAME }}"
"{{ .env.PROD_CLICKHOUSE_USERNAME }}"
```
Or, by creating a separate connector altogether.
```yaml
# /connectors/dev_clickhouse.yaml
# /connectors/prod_clickhouse.yaml
```
:::
## Environment-Specific Data Source Location
Rill allows you to configure different data source locations for different environments (development, staging, production) using templating. This enables you to work with development datasets locally while pointing to production data sources in cloud deployments.
```yaml
type: connector
driver: bigquery
dev:
project_id: rilldata_dev
project_id: rilldata
google_application_credentials: "{{ .env.GOOGLE_APPLICATION_CREDENTIALS }}"
```
```yaml
type: connector
driver: postgres
dev:
dsn: "postgres://user:password@localhost:5432/dev_database"
dsn: "postgres://user:password@localhost:5432/database"
```
## Additional Resources
- [Performance Optimization Guide](/developers/tutorials/performance)
- [Official docs](https://pkg.go.dev/text/template) (Go)
- [Learn Go Template Syntax](https://developer.hashicorp.com/nomad/tutorials/templates/go-template-syntax) (HashiCorp)
- [Sprig Function Documentation](http://masterminds.github.io/sprig/)
---
#### Custom Apis
---
###### Calling Custom APIs
---
title: Calling Custom APIs
description: How to call and consume your custom APIs from external applications
sidebar_label: Calling APIs
sidebar_position: 70
---
Custom APIs are exposed as HTTP endpoints that return JSON. You can call them from any language or tool that supports HTTP requests.
## API endpoints
### Rill Cloud
```
https://api.rilldata.com/v1/organizations//projects//runtime/api/
```
This targets the project's production deployment. To call the API on the deployment for a specific branch (e.g. a dev/preview deployment), insert `/branch/` before `/runtime`:
```
https://api.rilldata.com/v1/orgs//projects//branch//runtime/api/
```
### Local development
```
http://localhost:9009/v1/instances/default/api/
```
Where `` is the name of your API file without the `.yaml` extension (e.g., `my-api.yaml` → `my-api`).
## Making requests
Custom APIs accept both GET and POST requests.
### GET with query parameters
```bash
curl "https://api.rilldata.com/v1/organizations//projects//runtime/api/my-api?domain=google.com&limit=10" \
-H "Authorization: Bearer "
```
### POST with JSON body
```bash
curl -X POST "https://api.rilldata.com/v1/organizations//projects//runtime/api/my-api" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{"domain": "google.com", "limit": 10}'
```
Both methods produce the same result. If you provide both query parameters and a JSON body, query parameters take precedence.
### Response format
APIs return a JSON array of objects:
```json
[
{"publisher": "Facebook", "domain": "google.com", "total": 15234},
{"publisher": "Google", "domain": "google.com", "total": 12876}
]
```
## Testing locally
Local development does not require authentication:
```bash
# GET request
curl "http://localhost:9009/v1/instances/default/api/my-api?domain=google.com"
# POST request
curl -X POST http://localhost:9009/v1/instances/default/api/my-api \
-H "Content-Type: application/json" \
-d '{"domain": "google.com"}'
```
:::note
User attributes (`{{ .user.* }}`) are not available during local testing since no authentication token is provided. To test with user attributes, deploy to Rill Cloud and use a service token with [custom attributes](/developers/build/custom-apis/security#custom-attributes-on-service-tokens).
:::
## Authentication
Rill Cloud APIs require a bearer token in the `Authorization` header.
### For development and testing
Create a [user token](/guide/administration/access-tokens/user-tokens) (inherits your personal permissions):
```bash
rill token issue --display-name "API Testing"
# Returns: rill_usr_...
curl "https://api.rilldata.com/v1/organizations//projects//runtime/api/my-api" \
-H "Authorization: Bearer rill_usr_..."
```
### For production systems
Create a [service token](/guide/administration/access-tokens/service-tokens) with optional custom attributes:
```bash
rill service create my-api-service \
--project my-project \
--project-role viewer \
--attributes '{"customer_id": "acme-corp"}'
# Returns: rill_svc_...
```
Custom attributes on the token are available in your API templates as `{{ .user.customer_id }}`. See [Security & Access Control](/developers/build/custom-apis/security) for details on how to use custom attributes to build multi-tenant APIs.
:::note
If you need short-lived tokens with custom attributes for end users, use the [ephemeral token feature](/developers/build/custom-apis/security#issuing-ephemeral-tokens) instead of creating a service token per user.
:::
:::tip Token Documentation
For full guidance on token types, roles, and management:
- **[User Tokens](/guide/administration/access-tokens/user-tokens)** — Personal access tokens for development
- **[Service Tokens](/guide/administration/access-tokens/service-tokens)** — Long-lived tokens for production systems
- **[Roles and Permissions](/guide/administration/users-and-access/roles-permissions)** — Understand access levels
:::
## OpenAPI schema
Rill automatically generates an OpenAPI spec for your project. Download it to generate typed clients:
```bash
# From Rill Cloud
curl "https://api.rilldata.com/v1/organizations//projects//runtime/api/openapi" \
-H "Authorization: Bearer " \
-o openapi.json
# Locally
curl http://localhost:9009/v1/instances/default/api/openapi -o openapi.json
```
See [OpenAPI Documentation](/developers/build/custom-apis/openapi) for how to add request and response schemas to your API definitions.
---
###### Custom APIs
---
title: Custom APIs
description: Expose your Rill data as HTTP API endpoints
sidebar_label: Custom APIs
sidebar_position: 10
---
Rill lets you create custom API endpoints that return data from your project as JSON over HTTP. Define a YAML file, write a SQL query, and you have an API — no backend code required.
Custom APIs are ideal for:
- **Powering internal tools** — feed Rill data into dashboards, Slack bots, or scripts
- **Building customer-facing integrations** — expose filtered data to external applications
- **Automating workflows** — pull data into CI/CD pipelines, scheduled jobs, or ETL processes
- **Multi-tenant data access** — serve different data to different customers using [custom attributes](/developers/build/custom-apis/security)
## API types
Rill supports two types of custom APIs:
| Type | Best for | Query target |
|------|----------|-------------|
| [**SQL API**](/developers/build/custom-apis/sql) | Querying models, tables, or external databases directly | Any model, table, or external connector (DuckDB, BigQuery, Snowflake, etc.) |
| [**Metrics SQL API**](/developers/build/custom-apis/metrics-sql) | Querying metrics views using dimension and measure names | Metrics views (inherits security policies automatically) |
## Your first custom API
### 1. Create an API file
Create a YAML file in your project's `apis/` directory. For example, `apis/top-publishers.yaml`:
```yaml
type: api
sql: |
SELECT publisher, COUNT(*) as total_records
FROM ad_bids
GROUP BY publisher
ORDER BY total_records DESC
LIMIT 10
```
### 2. Test it locally
With Rill Developer running (`rill start`), call your API at:
```bash
curl "http://localhost:9009/v1/instances/default/api/top-publishers"
```
You'll get a JSON response:
```json
[
{"publisher": "Facebook", "total_records": 15234},
{"publisher": "Google", "total_records": 12876},
{"publisher": "Microsoft", "total_records": 9541}
]
```
:::note
Local development does not require authentication. When deployed to Rill Cloud, all API calls require a bearer token.
:::
### 3. Deploy and call from Rill Cloud
After deploying your project, call the API with authentication:
```bash
curl "https://api.rilldata.com/v1/organizations//projects//runtime/api/top-publishers" \
-H "Authorization: Bearer "
```
To test against the deployment for a specific branch (e.g. a dev/preview deployment), insert `/branch/` before `/runtime`:
```bash
curl "https://api.rilldata.com/v1/orgs//projects//branch//runtime/api/top-publishers" \
-H "Authorization: Bearer "
```
See [Calling APIs](/developers/build/custom-apis/calling) for full details on authentication and calling APIs.
## Make it dynamic
Add [templating](/developers/build/custom-apis/templating) to make your API accept parameters:
```yaml
type: api
sql: |
SELECT publisher, COUNT(*) as total_records
FROM ad_bids
WHERE domain = '{{ .args.domain }}'
GROUP BY publisher
ORDER BY total_records DESC
LIMIT {{ default 10 .args.limit }}
```
Call it with query parameters:
```bash
curl "http://localhost:9009/v1/instances/default/api/top-publishers?domain=google.com&limit=5"
```
## Add access control
Use [security rules](/developers/build/custom-apis/security) to control who can access your API and what data they see:
```yaml
type: api
sql: |
SELECT publisher, domain, COUNT(*) as total_records
FROM ad_bids
WHERE customer_id = '{{ .user.customer_id }}'
GROUP BY publisher, domain
security:
access: true
```
Each customer sees only their own data, based on the [custom attributes](/developers/build/custom-apis/security#custom-attributes-on-service-tokens) in their token.
## Next steps
- [**Templating**](/developers/build/custom-apis/templating) — Dynamic arguments, user attributes, and conditional logic
- [**Security & Access Control**](/developers/build/custom-apis/security) — Custom attributes, multi-tenant patterns, and access rules
- [**OpenAPI Documentation**](/developers/build/custom-apis/openapi) — Document your APIs with OpenAPI specs
- [**Calling APIs**](/developers/build/custom-apis/calling) — HTTP endpoints, authentication, and client generation
---
###### Metrics SQL APIs
---
title: Metrics SQL APIs
description: Create custom APIs that query metrics views using dimension and measure names
sidebar_label: Metrics SQL APIs
sidebar_position: 30
---
Metrics SQL APIs let you query [metrics views](/developers/build/metrics-view) using the dimension and measure names you've already defined. Instead of writing raw SQL against underlying tables, you write queries using your metrics view's semantic layer — and security policies are inherited automatically.
## Basic syntax
Create a YAML file in your project's `apis/` directory:
```yaml
type: api
metrics_sql: SELECT publisher, domain, total_records FROM ad_bids_metrics
```
## How Metrics SQL works
Metrics SQL transforms your query by replacing dimension and measure names with their underlying expressions. Consider this metrics view:
```yaml
# metrics/ad_bids_metrics.yaml
type: metrics_view
title: Ad Bids
model: ad_bids
timeseries: timestamp
dimensions:
- name: publisher
expression: toUpper(publisher)
- name: domain
column: domain
measures:
- name: total_records
expression: COUNT(*)
- name: avg_bid_price
expression: AVG(bid_price)
```
When you write:
```sql
SELECT publisher, domain, total_records FROM ad_bids_metrics
```
Rill translates this to:
```sql
SELECT toUpper(publisher) AS publisher, domain AS domain, COUNT(*) AS total_records
FROM ad_bids
GROUP BY publisher, domain
```
This means you write simple queries using business-friendly names, while Rill handles the underlying SQL complexity.
## Why use Metrics SQL over raw SQL?
| Benefit | Description |
|---------|-------------|
| **Reuse definitions** | Query using dimension/measure names defined once in your metrics view |
| **Automatic security** | Row-level security policies from the metrics view are applied automatically |
| **Simpler queries** | No need to remember complex expressions — just use names like `total_records` |
| **Consistency** | All APIs and dashboards use the same metric definitions |
## Examples
### Filtering with WHERE
```yaml
type: api
metrics_sql: |
SELECT publisher, total_records
FROM ad_bids_metrics
WHERE domain = 'google.com'
ORDER BY total_records DESC
LIMIT 10
```
### Aggregation with HAVING
```yaml
type: api
metrics_sql: |
SELECT publisher, total_records, avg_bid_price
FROM ad_bids_metrics
HAVING total_records > 1000
ORDER BY avg_bid_price DESC
```
### Pagination
```yaml
type: api
metrics_sql: |
SELECT publisher, domain, total_records
FROM ad_bids_metrics
ORDER BY total_records DESC
LIMIT 20 OFFSET 40
```
## Security inheritance
When you use Metrics SQL, any [security policies](/developers/build/metrics-view/security) defined on the metrics view are automatically enforced. For example, if your metrics view has:
```yaml
# In your metrics view
security:
access: true
row_filter: "domain = '{{ .user.domain }}'"
```
Then a Metrics SQL API querying this view will automatically filter rows based on the user's domain — no additional configuration needed in the API definition.
This is one of the key advantages of Metrics SQL over raw SQL APIs. See [Security & Access Control](/developers/build/custom-apis/security) for more on how security works with custom APIs.
## Supported SQL features
For a full reference on supported SQL syntax (SELECT, WHERE, HAVING, ORDER BY, LIMIT, OFFSET) and current limitations, see the [Metrics SQL language reference](/developers/build/metrics-view/metrics-sql).
## Adding dynamic behavior
Metrics SQL APIs support the same [templating](/developers/build/custom-apis/templating) as SQL APIs:
```yaml
type: api
metrics_sql: |
SELECT publisher, total_records
FROM ad_bids_metrics
{{ if hasKey .args "domain" }}
WHERE domain = '{{ .args.domain }}'
{{ end }}
ORDER BY total_records DESC
LIMIT {{ default 25 .args.limit }}
```
See [Dynamic Queries with Templating](/developers/build/custom-apis/templating) for the full guide.
---
###### OpenAPI Documentation
---
title: OpenAPI Documentation
description: Add OpenAPI specs to your custom APIs for documentation and client generation
sidebar_label: OpenAPI Documentation
sidebar_position: 60
---
Rill automatically generates an OpenAPI specification for your project that combines built-in APIs with your custom API definitions. You can add request and response schemas to your APIs for better documentation and typed client generation.
## Adding an OpenAPI spec to your API
Add an `openapi` block to your API definition with `request_schema` and `response_schema`:
```yaml
type: api
sql: |
SELECT publisher, COUNT(*) as total_records
FROM ad_bids
WHERE domain = '{{ .args.domain }}'
{{ if hasKey .args "publisher" }}
AND publisher = '{{ .args.publisher }}'
{{ end }}
ORDER BY total_records DESC
LIMIT {{ default 25 .args.limit }}
OFFSET {{ default 0 .args.offset }}
openapi:
summary: Get ad bid statistics by publisher, filtered by domain
request_schema:
type: object
required:
- domain
properties:
domain:
type: string
description: Domain to filter results by
publisher:
type: string
description: Optional publisher filter
limit:
type: integer
description: Number of results to return (default 25)
offset:
type: integer
description: Offset for pagination (default 0)
response_schema:
type: object
properties:
publisher:
type: string
description: Publisher name
total_records:
type: integer
description: Total number of ad bid records
```
### OpenAPI fields
| Field | Description |
|-------|-------------|
| `openapi.summary` | Short description of what the API does (appears in OpenAPI docs) |
| `openapi.request_schema` | JSON Schema describing the request parameters |
| `openapi.response_schema` | JSON Schema describing a single row in the response array |
Schemas follow the [JSON Schema](https://json-schema.org/) format. You can use `type`, `required`, `properties`, `description`, and other standard JSON Schema keywords.
## Downloading the OpenAPI spec
### Locally
```bash
curl http://localhost:9009/v1/instances/default/api/openapi -o openapi.json
```
### From Rill Cloud
```bash
curl "https://api.rilldata.com/v1/organizations//projects//runtime/api/openapi" \
-H "Authorization: Bearer " \
-o openapi.json
```
To download the spec for the deployment of a specific branch (e.g. a dev/preview deployment), insert `/branch/` before `/runtime`:
```bash
curl "https://api.rilldata.com/v1/orgs//projects//branch//runtime/api/openapi" \
-H "Authorization: Bearer " \
-o openapi.json
```
The generated spec includes all your custom APIs with their schemas, plus Rill's built-in API endpoints.
## Generating typed clients
Use the downloaded OpenAPI spec with any code generation tool to create typed clients for your language:
**JavaScript/TypeScript** (using [openapi-typescript](https://github.com/openapi-ts/openapi-typescript)):
```bash
npx openapi-typescript openapi.json -o ./src/api-types.ts
```
**Python** (using [openapi-python-client](https://github.com/openapi-generators/openapi-python-client)):
```bash
openapi-python-client generate --path openapi.json
```
**Go** (using [oapi-codegen](https://github.com/oapi-codegen/oapi-codegen)):
```bash
oapi-codegen -package api openapi.json > api/client.go
```
## Full example
Here's a complete API with OpenAPI documentation, templating, and security:
```yaml
type: api
metrics_sql: |
SELECT publisher, domain, total_records, avg_bid_price
FROM ad_bids_metrics
WHERE domain = '{{ .args.domain }}'
{{ if hasKey .args "publisher" }}
AND publisher = '{{ .args.publisher }}'
{{ end }}
ORDER BY total_records DESC
LIMIT {{ default 25 .args.limit }}
OFFSET {{ default 0 .args.offset }}
security:
access: true
openapi:
summary: Query ad bid metrics by domain with optional publisher filter
request_schema:
type: object
required:
- domain
properties:
domain:
type: string
description: Domain to filter metrics by (e.g., "google.com")
publisher:
type: string
description: Optional publisher to filter by (e.g., "Facebook")
limit:
type: integer
description: Max results to return (default 25, max 1000)
offset:
type: integer
description: Pagination offset (default 0)
response_schema:
type: object
properties:
publisher:
type: string
description: Publisher name
domain:
type: string
description: Domain name
total_records:
type: integer
description: Total number of records
avg_bid_price:
type: number
description: Average bid price in USD
```
---
###### Security & Access Control
---
title: Security & Access Control
description: Control who can access your APIs and what data they see using security rules and custom attributes
sidebar_label: Security & Access Control
sidebar_position: 50
---
Rill's custom APIs support fine-grained access control through security rules and custom attributes on tokens. You can restrict who can call an API, and filter the data each caller sees — all without writing backend code.
## API access rules
Control who can access an API using the `security` block:
### Allow all authenticated users
```yaml
type: api
sql: SELECT publisher, COUNT(*) as total FROM ad_bids GROUP BY publisher
security:
access: true
```
### Restrict to admins only
```yaml
type: api
sql: |
SELECT publisher, SUM(revenue) as total_revenue
FROM ad_bids
GROUP BY publisher
security:
access: "{{ .user.admin }}"
```
Only users with admin permissions on the project can call this API. Non-admins receive a 403 Forbidden response.
### Restrict by custom attribute
```yaml
type: api
sql: SELECT * FROM internal_reports
security:
access: "{{ eq .user.tier \"enterprise\" }}"
```
Only users whose token has `tier: "enterprise"` can access this endpoint.
## Custom attributes on service tokens
Custom attributes are key-value pairs you attach to [service tokens](/guide/administration/access-tokens/service-tokens). When a service token is used to call an API, its attributes are available in templates as `{{ .user. }}`.
### Creating a service token with attributes
```bash
rill service create acme-api \
--project my-project \
--project-role viewer \
--attributes '{"customer_id": "acme-corp", "region": "us-west", "tier": "premium"}'
```
This creates a token with three custom attributes: `customer_id`, `region`, and `tier`.
### Updating attributes on an existing service
```bash
rill service edit acme-api \
--attributes '{"customer_id": "acme-corp", "region": "eu-central", "tier": "enterprise"}'
```
### Common attribute patterns
| Attribute | Use case |
|-----------|----------|
| `customer_id` | Multi-tenant data isolation |
| `region` | Geographic data filtering |
| `department` | Departmental access control |
| `tier` | Feature gating (free, premium, enterprise) |
| `environment` | Environment-specific data (production, staging) |
## How attributes flow through the system
When an API is called with a service token, here's what happens:
```
1. Service token created with attributes: {"customer_id": "acme"}
↓
2. API call with bearer token
↓
3. Rill extracts attributes from the token into JWT claims
↓
4. Template engine makes attributes available as {{ .user.customer_id }}
↓
5. SQL query is rendered with the actual values
↓
6. Query executes and returns filtered results
```
## End-to-end example: multi-tenant API
This walkthrough shows how to build an API that serves different data to different customers.
### Step 1: Create the API
Create `apis/customer-orders.yaml`:
```yaml
type: api
sql: |
SELECT
order_id,
product_name,
quantity,
total_price,
order_date
FROM orders
WHERE customer_id = '{{ .user.customer_id }}'
ORDER BY order_date DESC
LIMIT {{ default 50 .args.limit }}
OFFSET {{ default 0 .args.offset }}
security:
access: true
```
### Step 2: Create service tokens for each customer
```bash
# Token for Acme Corp
rill service create acme-api \
--project my-project \
--project-role viewer \
--attributes '{"customer_id": "acme-corp"}'
# Returns: rill_svc_abc123...
# Token for Globex Inc
rill service create globex-api \
--project my-project \
--project-role viewer \
--attributes '{"customer_id": "globex-inc"}'
# Returns: rill_svc_def456...
```
### Step 3: Call the API
**Acme sees only their orders:**
```bash
curl "https://api.rilldata.com/v1/organizations/my-org/projects/my-project/runtime/api/customer-orders" \
-H "Authorization: Bearer rill_svc_abc123..."
```
```json
[
{"order_id": "A-1001", "product_name": "Widget Pro", "quantity": 50, "total_price": 2500, "order_date": "2025-01-15"},
{"order_id": "A-1002", "product_name": "Gadget Plus", "quantity": 25, "total_price": 1250, "order_date": "2025-01-14"}
]
```
**Globex sees only their orders:**
```bash
curl "https://api.rilldata.com/v1/organizations/my-org/projects/my-project/runtime/api/customer-orders" \
-H "Authorization: Bearer rill_svc_def456..."
```
```json
[
{"order_id": "G-2001", "product_name": "Sprocket X", "quantity": 100, "total_price": 5000, "order_date": "2025-01-16"},
{"order_id": "G-2002", "product_name": "Bolt Kit", "quantity": 200, "total_price": 800, "order_date": "2025-01-13"}
]
```
Same API, same endpoint — different data based on the token's `customer_id` attribute.
## Admin vs non-admin patterns
Use `{{ .user.admin }}` to expose different data or behavior based on the user's role:
### Show extra columns for admins
```yaml
type: api
sql: |
SELECT
publisher,
domain,
COUNT(*) as impressions
{{ if .user.admin }}
, SUM(revenue) as total_revenue
, AVG(cost_per_click) as avg_cpc
{{ end }}
FROM ad_bids
GROUP BY publisher, domain
ORDER BY impressions DESC
LIMIT 50
```
### Remove filters for admins
```yaml
type: api
sql: |
SELECT publisher, domain, COUNT(*) as total
FROM ad_bids
WHERE 1=1
{{ if (not .user.admin) }}
AND customer_id = '{{ .user.customer_id }}'
{{ end }}
GROUP BY publisher, domain
```
Admins see all data across all customers; non-admins see only their customer's data.
## Metrics SQL security inheritance
When using [Metrics SQL APIs](/developers/build/custom-apis/metrics-sql), security policies defined on the metrics view are automatically enforced. You don't need to add a `security` block to the API — it inherits the metrics view's policies:
```yaml
# metrics/ad_bids_metrics.yaml
type: metrics_view
model: ad_bids
security:
access: true
row_filter: "customer_id = '{{ .user.customer_id }}'"
```
```yaml
# apis/customer-metrics.yaml
type: api
metrics_sql: |
SELECT publisher, total_records
FROM ad_bids_metrics
ORDER BY total_records DESC
```
The `row_filter` from the metrics view is automatically applied — each customer only sees their own data, even though the API definition doesn't mention security at all.
## Skipping nested security
By default, when an API queries a metrics view, Rill enforces the security policies on both the API itself and the underlying metrics view. In some cases, you may want the API to handle all access control itself and skip checks on nested resources:
```yaml
type: api
sql: |
SELECT * FROM sensitive_model
WHERE access_level <= {{ .user.access_level }}
security:
access: true
skip_nested_security: true
```
Use `skip_nested_security: true` when your API already handles all necessary access control in its own query logic.
## Issuing ephemeral tokens
For applications that need to issue short-lived tokens to end users (e.g., for embedded dashboards or temporary API access), service tokens can issue ephemeral tokens with custom user attributes:
```bash
curl -X POST "https://api.rilldata.com/v1/orgs//projects//credentials" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"attributes": {
"email": "user@acme.com",
"customer_id": "acme-corp",
"department": "engineering"
},
"ttl_seconds": 3600
}'
```
The response contains a short-lived JWT that can be used to call APIs with those attributes. See [Service Tokens](/guide/administration/access-tokens/service-tokens#issuing-ephemeral-tokens) for details.
---
###### SQL APIs
---
title: SQL APIs
description: Create custom APIs with SQL queries against models, tables, and external databases
sidebar_label: SQL APIs
sidebar_position: 20
---
SQL APIs let you write SQL queries and expose them as HTTP endpoints. By default, queries execute against your project's default [OLAP connector](/developers/build/connectors/olap) (e.g., DuckDB, ClickHouse). You can also query external databases like BigQuery, Snowflake, and Postgres by specifying a different `connector`.
## Basic syntax
Create a YAML file in your project's `apis/` directory:
```yaml
type: api
sql: SELECT publisher, domain, bid_price FROM ad_bids LIMIT 100
```
This queries your default OLAP connector and returns the results as JSON.
### Multi-line queries
For longer queries, use YAML multi-line syntax:
```yaml
type: api
sql: |
SELECT
publisher,
domain,
COUNT(*) as impressions,
AVG(bid_price) as avg_bid
FROM ad_bids
WHERE timestamp >= '2024-01-01'
GROUP BY publisher, domain
ORDER BY impressions DESC
LIMIT 50
```
## Querying models and tables
By default, SQL APIs execute against your project's default [OLAP connector](/developers/build/connectors/olap). You can query any [model](/developers/build/models) or source table:
```yaml
# Query a model
type: api
sql: SELECT * FROM my_model WHERE status = 'active' LIMIT 100
```
```yaml
# Query with joins across models
type: api
sql: |
SELECT
o.order_id,
o.total,
c.name as customer_name
FROM orders o
JOIN customers c ON o.customer_id = c.id
ORDER BY o.total DESC
LIMIT 25
```
## Querying external databases
You can query external databases directly by specifying a `connector`. This lets you access data in real-time without ingesting it into Rill.
### Data warehouses
**Athena:**
```yaml
type: api
connector: athena
sql: SELECT * FROM s3_data_table WHERE event_date >= '2024-01-01' LIMIT 100
```
**BigQuery:**
```yaml
type: api
connector: bigquery
sql: SELECT * FROM `my-project.my_dataset.my_table` WHERE region = 'us' LIMIT 100
```
**Redshift:**
```yaml
type: api
connector: redshift
sql: SELECT * FROM transactions WHERE transaction_date >= '2024-01-01' LIMIT 100
```
**Snowflake:**
```yaml
type: api
connector: snowflake
sql: SELECT * FROM my_database.my_schema.events WHERE created_at >= '2024-01-01' LIMIT 100
```
### Databases
**MySQL:**
```yaml
type: api
connector: mysql
sql: SELECT * FROM orders WHERE order_date >= '2025-01-01' LIMIT 100
```
**Postgres:**
```yaml
type: api
connector: postgres
sql: SELECT * FROM events WHERE created_at >= '2025-01-01' LIMIT 100
```
:::warning External database costs
Queries to external connectors execute directly on your data source and incur costs based on your provider's billing model:
- **Athena / BigQuery** — charged per TB of data scanned
- **Redshift / Snowflake** — charged for compute time
- **MySQL / Postgres** — may incur costs based on instance compute and IOPS
To minimize costs: use `LIMIT` clauses, apply filters to reduce data scanned, and consider materializing frequently accessed queries as [models](/developers/build/models) in DuckDB.
:::
## When to use external connectors vs your OLAP engine
| Factor | OLAP engine (default) | External connector |
|--------|-------------------|--------------------|
| **Query speed** | Fast — data is local | Depends on source (network + query time) |
| **Data freshness** | As of last refresh | Real-time from the source |
| **Cost** | No additional cost | Per-query costs from your provider |
| **Best for** | Low-latency APIs, pre-modeled data | Real-time access, ad-hoc queries |
**Use your OLAP engine** when you need fast, low-cost queries against data already modeled in Rill. Your data refreshes on a schedule and is optimized for analytical queries.
**Use external connectors** when you need real-time access to the latest data, your data lives in the source database, or you're building internal tools where query costs are acceptable.
## Adding dynamic behavior
SQL APIs support [templating](/developers/build/custom-apis/templating) for dynamic arguments, user attributes, and conditional logic:
```yaml
type: api
sql: |
SELECT publisher, COUNT(*) as total
FROM ad_bids
WHERE domain = '{{ .args.domain }}'
{{ if .user.admin }}
AND internal_flag IS NOT NULL
{{ end }}
GROUP BY publisher
LIMIT {{ default 100 .args.limit }}
```
See [Dynamic Queries with Templating](/developers/build/custom-apis/templating) for the full guide.
---
###### Dynamic Queries with Templating
---
title: Dynamic Queries with Templating
description: Use templating to make your custom APIs dynamic with arguments, user attributes, and conditional logic
sidebar_label: Templating
sidebar_position: 40
---
Rill's custom APIs support Go-style templating to make your SQL queries dynamic. You can accept parameters from API callers, reference user attributes from authentication tokens, and use conditional logic to build flexible endpoints.
Templating works with both [SQL APIs](/developers/build/custom-apis/sql) and [Metrics SQL APIs](/developers/build/custom-apis/metrics-sql).
## Template context reference
Every template has access to the following context:
| Variable | Type | Description |
|----------|------|-------------|
| `{{ .args. }}` | `any` | Runtime arguments passed via query parameters or POST body |
| `{{ .user.email }}` | `string` | Authenticated user's email address |
| `{{ .user.domain }}` | `string` | Email domain of the authenticated user |
| `{{ .user.name }}` | `string` | Display name of the authenticated user |
| `{{ .user.admin }}` | `bool` | Whether the user has admin permissions on the project |
| `{{ .user.groups }}` | `[]string` | User groups the authenticated user belongs to |
| `{{ .user. }}` | `any` | Custom attributes from service tokens (e.g., `customer_id`, `region`) |
| `{{ .export }}` | `bool` | `true` when the API is being resolved for export (CSV, Excel, Parquet) |
:::note
When testing locally (`localhost:9009`), `.user` attributes are not available since no authentication is required. To test with user attributes, deploy to Rill Cloud and use a service token with [custom attributes](/developers/build/custom-apis/security#custom-attributes-on-service-tokens).
:::
## Dynamic arguments
Pass arguments to your API via query parameters (GET) or JSON body (POST). Reference them with `{{ .args. }}`:
```yaml
type: api
sql: |
SELECT publisher, domain, COUNT(*) as total
FROM ad_bids
WHERE domain = '{{ .args.domain }}'
GROUP BY publisher, domain
```
**Calling the API:**
```bash
# Via query parameter
curl "http://localhost:9009/v1/instances/default/api/my-api?domain=google.com"
# Via POST body
curl -X POST http://localhost:9009/v1/instances/default/api/my-api \
-H "Content-Type: application/json" \
-d '{"domain": "google.com"}'
```
### Multiple arguments
```yaml
type: api
sql: |
SELECT publisher, domain, COUNT(*) as total
FROM ad_bids
WHERE domain = '{{ .args.domain }}'
AND publisher = '{{ .args.publisher }}'
GROUP BY publisher, domain
```
```bash
curl "http://localhost:9009/v1/instances/default/api/my-api?domain=google.com&publisher=Facebook"
```
## User attributes
Reference the authenticated user's attributes with `{{ .user. }}`. These come from the user's identity (for user tokens) or from custom attributes (for service tokens).
### Built-in attributes
```yaml
type: api
sql: |
SELECT *
FROM reports
WHERE owner_email = '{{ .user.email }}'
LIMIT 50
```
### Custom attributes from service tokens
[Service tokens with custom attributes](/developers/build/custom-apis/security#custom-attributes-on-service-tokens) are available in templates as `{{ .user. }}`:
```yaml
type: api
sql: |
SELECT order_id, product, total
FROM orders
WHERE customer_id = '{{ .user.customer_id }}'
AND region = '{{ .user.region }}'
```
See [Security & Access Control](/developers/build/custom-apis/security) for creating tokens and a full multi-tenant walkthrough.
## Conditional logic
Use Go template `{{ if }}` / `{{ else }}` / `{{ end }}` blocks to conditionally include SQL:
### Admin-only columns
```yaml
type: api
sql: |
SELECT
publisher,
COUNT(*) as total_records
{{ if .user.admin }}
, SUM(revenue) as total_revenue
, AVG(bid_price) as avg_bid
{{ end }}
FROM ad_bids
GROUP BY publisher
ORDER BY total_records DESC
```
Admins see revenue and bid data; non-admins see only publisher and record count.
### Admin-only filters
```yaml
type: api
sql: |
SELECT publisher, domain, COUNT(*) as total
FROM ad_bids
WHERE timestamp >= '{{ .args.start_date }}'
{{ if (not .user.admin) }}
AND domain = '{{ .user.domain }}'
{{ end }}
GROUP BY publisher, domain
```
Non-admins see only data for their domain; admins see everything.
## Optional parameters
Use the `hasKey` function to check whether an argument was provided, making parameters optional:
```yaml
type: api
sql: |
SELECT publisher, COUNT(*) as total_records
FROM ad_bids
WHERE 1=1
{{ if hasKey .args "publisher" }}
AND publisher = '{{ .args.publisher }}'
{{ end }}
{{ if hasKey .args "domain" }}
AND domain = '{{ .args.domain }}'
{{ end }}
GROUP BY publisher
ORDER BY total_records DESC
```
**Without parameters** — returns all publishers:
```bash
curl "http://localhost:9009/v1/instances/default/api/my-api"
```
**With one parameter** — filters by publisher:
```bash
curl "http://localhost:9009/v1/instances/default/api/my-api?publisher=Google"
```
**With both** — filters by publisher and domain:
```bash
curl "http://localhost:9009/v1/instances/default/api/my-api?publisher=Google&domain=news.google.com"
```
## Pagination pattern
Build paginated APIs using `LIMIT` and `OFFSET` with the `default` function for sensible defaults:
```yaml
type: api
sql: |
SELECT publisher, domain, bid_price, timestamp
FROM ad_bids
ORDER BY timestamp DESC
LIMIT {{ default 25 .args.limit }}
OFFSET {{ default 0 .args.offset }}
```
**Page 1 (first 25 results):**
```bash
curl "http://localhost:9009/v1/instances/default/api/my-api"
```
**Page 2:**
```bash
curl "http://localhost:9009/v1/instances/default/api/my-api?offset=25"
```
**Custom page size:**
```bash
curl "http://localhost:9009/v1/instances/default/api/my-api?limit=10&offset=20"
```
## Sprig utility functions
Rill uses standard Go templating together with [Sprig](http://masterminds.github.io/sprig/), which provides many utility functions. Commonly used ones:
| Function | Example | Description |
|----------|---------|-------------|
| `default` | `{{ default 100 .args.limit }}` | Use a default value if the argument is empty |
| `hasKey` | `{{ if hasKey .args "name" }}` | Check if an argument was provided |
| `lower` | `{{ lower .args.status }}` | Convert to lowercase |
| `upper` | `{{ upper .args.code }}` | Convert to uppercase |
| `trim` | `{{ trim .args.name }}` | Trim whitespace |
| `ne` | `{{ if (ne .user.domain "") }}` | Not equal comparison |
| `eq` | `{{ if (eq .user.region "us") }}` | Equal comparison |
### Example: combining multiple patterns
Here's a real-world example combining optional params, defaults, user attributes, and conditionals:
```yaml
type: api
sql: |
SELECT
publisher,
domain,
COUNT(*) as impressions,
AVG(bid_price) as avg_bid
{{ if .user.admin }}
, SUM(revenue) as total_revenue
{{ end }}
FROM ad_bids
WHERE 1=1
{{ if hasKey .args "publisher" }}
AND publisher = '{{ .args.publisher }}'
{{ end }}
{{ if hasKey .args "start_date" }}
AND timestamp >= '{{ .args.start_date }}'
{{ end }}
{{ if (not .user.admin) }}
AND domain = '{{ .user.domain }}'
{{ end }}
GROUP BY publisher, domain
ORDER BY impressions DESC
LIMIT {{ default 50 .args.limit }}
OFFSET {{ default 0 .args.offset }}
```
This single endpoint handles:
- Optional filtering by publisher and date range
- Admin users see revenue; non-admins don't
- Non-admin users are scoped to their domain
- Pagination with sensible defaults
---
#### Dashboards
---
##### Canvas Widgets
---
####### All Canvas Widgets
---
title: All Canvas Widgets
description: Complete guide to all available widgets in Rill Canvas Dashboards
toc_max_heading_level: 3
className: connect-connect
---
import ComponentTile from '@site/src/components/ComponentTile';
Rill Canvas dashboards are built using a variety of widgets that can display data, create visualizations, and add rich content. Each component can be created dynamically through the visual Canvas dashboard editor or defined in individual YAML files. For more information, refer to our [Components reference doc](/reference/project-files/component).
## Data
### KPI
### Leaderboard
### Pivot / Table
}
/>
}
/>
}
/>
## Chart
### Bar
### Line
### Stacked Area
### Stacked Bar
### Stacked Bar Normalized
### Donut
### Funnel
### Heat Map
### Combo
### Scatter Plot
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
## Miscellaneous
### Markdown Text
### Image
### Component
}
/>
}
/>
}
/>
---
####### "Chart Widgets"
---
title: "Chart Widgets"
sidebar_label: "Chart"
sidebar_position: 10
---
import ImageCodeToggle from '@site/src/components/ImageCodeToggle';
Chart widgets in Rill Canvas allow you to visualize your data in various formats. You can create charts dynamically in the Canvas Dashboard or through individual component files. For more information, refer to our [Components reference doc](/reference/project-files/component).
## Bar Chart
Bar charts are ideal for comparing values across different categories.
## Line Chart
Line charts are perfect for showing trends over time.
## Stacked Area Chart
Area charts show the magnitude of change over time with filled areas.
## Stacked Bar Chart
Stacked bar charts show multiple data series stacked on top of each other.
## Stacked Bar Normalized
Normalized stacked bars show proportions instead of absolute values.
## Donut Chart
Donut charts display data as segments of a circle with a hollow center.
## Funnel Chart
Funnel charts show the flow through a process with decreasing/increasing values at each stage.
## Heat Map
Heat maps visualize data density using color intensity across two dimensions.
## Combo Chart
Combo charts combine different chart types (like bars and lines) in a single visualization.
## Scatter Plot
Scatter plots visualize the relationship between two measures, with each point representing a data value. Points can be colored by a dimension and optionally sized by a measure. Scatter plots support interactive zooming and panning.
**Interactive Controls:**
- **Shift + Drag**: Pan the plot
- **Scroll Wheel**: Zoom in/out
- **Double-click**: Reset zoom and pan to default view
---
####### "Data Widgets"
---
title: "Data Widgets"
sidebar_label: "Data"
sidebar_position: 00
---
import ImageCodeToggle from '@site/src/components/ImageCodeToggle';
Data widgets in Rill Canvas allow you to display raw data in various formats. These widgets are perfect for showing detailed information, metrics, and tabular data. For more information, refer to our [Components reference doc](/reference/project-files/component).
## KPI Grid
KPI grids display key performance indicators in a compact grid format with comparison capabilities. You can select up to 10 concurrent measures to display in a single widget
## Leaderboard
Leaderboards show ranked data with the top performers highlighted.
## Pivot/Table
Tables display detailed data in a structured format with customizable columns.
## Navigation
All Data widgets also provide a button to "Go to explore" that can navigate to the Explore dashboard if available.

---
####### "Miscellaneous Widgets"
---
title: "Miscellaneous Widgets"
sidebar_label: "Miscellaneous"
sidebar_position: 20
---
import ImageCodeToggle from '@site/src/components/ImageCodeToggle';
Miscellaneous widgets in Rill Canvas provide additional functionality for text, images, and other non-data elements. These widgets help enhance your dashboards with rich content. For more information, refer to our [Components reference doc](/reference/project-files/component).
## Text/Markdown
Text widgets allow you to add formatted text, markdown content, and documentation directly to your dashboards. You can also use `metrics_sql` and `metrics_sql_rows` to display data from Metrics SQL queries.
### Dynamic Markdown
The `metrics_sql` template function allows you to execute a Metrics SQL query and display the results as formatted text in your dashboard. This is useful for displaying aggregated data or custom metrics.
The `metrics_sql_rows` template function allows you to execute a Metrics SQL query and iterate over the results, displaying each row with custom formatting. This is useful for displaying multiple data points in a structured format.
Use `metrics_sql_rows` to get query results and then iterate over them using Go template syntax. You can access columns using dot notation (`.column_name`) or by assigning a variable name in the range:
{{ $row.publisher }}
Domain: {{ $row.adomain }}
Total: {{ $row.total_bids }}
{{ end }}
width: 6`}
codeLanguage="yaml"
/>
Alternatively, you can use dot notation directly without assigning a variable:
```yaml
- markdown:
content: |-
{{ $rows := metrics_sql_rows "SELECT publisher, total_records FROM ad_bids_metrics LIMIT 5" }}
{{ range $rows }}
{{ .publisher }}: {{ .total_records }}
{{ end }}
```
The `metrics_sql_rows` function returns an array of rows that you can iterate over with `{{ range }}`. Access columns using either `{{ $row.column_name }}` (when using named variables) or `{{ .column_name }}` (when using dot notation).
For more information on Metrics SQL syntax and capabilities, see the [Metrics SQL API documentation](/developers/build/metrics-view/metrics-sql).
## Image
Image widgets let you embed images, logos, and visual elements into your dashboards. Put files in your public/ folder and reference them directly in the `url` as `public/image.png`.
## Component
Reference a reusable component created outside of the canvas dashboard.
---
###### Canvas Dashboards
---
title: Canvas Dashboards
description: Create custom dashboards by assembling visualizations and components
sidebar_label: Canvas Dashboards
sidebar_position: 05
---
While Rill's signature **[Explore dashboards](/developers/build/dashboards/explore)** let you slice, dice, and interact with your data in our suggested layout, **Canvas dashboards** let you define your own visualizations and arrange them into your preferred layout.
Canvas dashboards are built using various components that can display data, create visualizations, and add rich content from independent metrics views. You can create components dynamically through the visual Canvas Dashboard interface or define them in individual YAML files.
## Available Components
Canvas dashboards support three main types of components:
- **[Data components](/developers/build/dashboards/canvas-widgets/data)** - Display structured data and key metrics (KPIs, leaderboards, tables)
- **[Chart components](/developers/build/dashboards/canvas-widgets/chart)** - Create rich visualizations (bar charts, line charts, heat maps, etc.)
- **[Miscellaneous components](/developers/build/dashboards/canvas-widgets/misc)** - Add text, images, and other content
Each component page shows both the visual result and the corresponding YAML code, making it easy to understand how to implement them in your own dashboards.
For a complete overview of all available components, see our [**Canvas components**](/developers/build/dashboards/canvas-widgets) reference.
## Creating a Canvas Dashboard
### A Visual Editing Experience
To modify any single widget, click to select it and use the right-hand panel to change its associated properties. Click outside the widget to view global properties associated with the overall Canvas.

### Filters
Optionally toggle on the **global filter bar** under Canvas properties to give dashboard viewers access to the same time and dimension filters available on Explore dashboards.

**Local filters** for a single KPI, Chart, or Table can be separated from the global filters via the "Filters" tab in the properties panel, where you can set filters that are local to just that widget.

### Making changes to the YAML
While we encourage creating Canvas dashboards via the visual editing experience described above, you can always edit the YAML file directly using the code view by toggling the switch next to the filename at the top of the page. Please see our [customization page](/developers/build/dashboards/customization) and [reference documentation](/reference/project-files/canvas-dashboards) for more information.

:::tip Customize default time ranges
Set project-wide default time ranges and available options for all canvas dashboards.
[Learn more about canvas defaults →](/developers/build/project-configuration#canvas-defaults)
:::
## Default Filters
Dashboard creators can configure default filters to establish a consistent starting point for viewers. Filters are defined as Metrics SQL WHERE expressions, keyed by the metrics view name they apply to.
```yaml
defaults:
filters:
# Key is the metrics view name; value is a Metrics SQL WHERE expression
my_metrics_view: "country IN ('US', 'CA') AND revenue > 1000"
another_metrics_view: "status = 'active'"
```
This lets you pre-filter data across one or more metrics views used in the canvas, ensuring users begin their analysis with the most relevant context.
For detailed YAML configurations, see the [`defaults`](/reference/project-files/canvas-dashboards#defaults) section in our reference documentation.
## Example Canvas Dashboards
Here are a few deployed examples of Canvas dashboards that you can check out!
- **[E-commerce demo dashboard](https://ui.rilldata.com/demo/ezcommerce-demo/canvas/canvas)**
- **[Programmatic advertising demo dashboard](https://ui.rilldata.com/demo/rill-openrtb-prog-ads/canvas/executive_overview)**
- **[New York City demo dashboard](https://ui.rilldata.com/demo/nyc-canvas-jam/canvas/scorecard%20canvas)**
- **[NYC party demo dashboard 🎉](https://ui.rilldata.com/demo/nyc-canvas-jam/canvas/Leaderboard)**
---
###### "Customization & Themes"
---
title: "Customization & Themes"
description: Alter dashboard look and feel
sidebar_label: "Customization & Themes"
sidebar_position: 30
---
Below are some common customizations and dashboard configurations available for end users.
:::info Dashboard properties
For a full list of available dashboard properties and configurations, please see our [Dashboard YAML](/reference/project-files/explore-dashboards) reference page.
:::
## Define Dashboard Access
Along with [metrics views security policies](/developers/build/metrics-view/security), you can set access on the dashboard level. Access policies will be combined with metrics view policies using a logical AND, so if a user doesn’t pass both, they won’t get access to the dashboard. Only the `access` key can be set in the dashboard.
```yaml
security:
access: "{{ .user.admin }} OR '{{ .user.domain }}' == 'example.com'"
```
## Setting Default Views for Dashboards
### Default Time Range
Default time range controls the data analyzed on initial page load. Setting the default time range improves user experience by setting it to the most frequently used period— in particular, avoiding `all time` if you have a large data source but only analyze more recent data.
The value must be either a valid [ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations) (for example, `PT12H` for 12 hours, `P1M` for 1 month, or `P26W` for 26 weeks) or one of the [Rill ISO 8601 extensions](/reference/time-syntax/rill-iso-extensions#extensions).
### Default Dimensions
For dashboards with wide tables, setting default dimensions is a good way to make sure that users can focus on the primary analyses and ensure a positive first experience. Each dimension listed under the `dimensions` setting will appear on the screen, while the remainder of the dimensions will be hidden (and still available for selection under filters). Common use cases for setting default fields include simplifying dashboards on initial load, narrowing the dashboard to the most used fields, and avoiding high cardinality fields (that may take longer to load, but are used less often, so this improves performance). An example addition to the `dashboard.yaml` file is below.
```yaml
defaults:
dimensions:
- column1
- column2
```
:::warning Column vs. Name Usage
The `column` property is used by default from the column name in your underlying source. If you decide to use the `name` property, you'd replace the column above with the field name.
:::
### Default Measures
A list of measures that should be visible by default. Operates the same as the `default_dimensions` configuration. When selecting measures, by default, consider hiding more computationally intensive measures like `count distinct` or other complicated expressions to improve performance.
```yaml
defaults:
measures:
- measure_1
- measure_1
```
### Default Comparison Modes
It is also possible to set up a default comparison mode for your dashboard. In Rill, we support both [time comparison](/guide/dashboards/time-series#time-comparisons) and [dimension comparison.](/guide/dashboards/filters#filter-by-dimensions) Note that only one of these comparisons can be set as default.
```yaml
defaults:
comparison_mode: time
# comparison_mode: dimension
# comparison_dimension: action
```
## Time Ranges
One of the more important configurations, available time ranges allow you to change the defaults in the time dropdown for periods to select. Updating this list allows users to quickly change between the most common analyses, like day over day, recent weeks, or period to date. The range must be a valid [ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations) or one of the [Rill ISO 8601 extensions](/reference/time-syntax/rill-iso-extensions#extensions).
```yaml
time_ranges:
- PT15M
- PT1H
- P7D
- P4W
- rill-TD ## Today
- rill-WTD ## Week-To-date
```
## Time Zones
Rill will automatically select several time zones that should be pinned to the top of the time zone selector. It should be a list of [IANA time zone identifiers](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). You can add or remove relevant time zones for your team from this list.
```yaml
time_zones:
- America/Los_Angeles
- America/Chicago
- America/New_York
- Europe/London
- Europe/Paris
- Asia/Jerusalem
- Europe/Moscow
- Asia/Kolkata
- Asia/Shanghai
- Asia/Tokyo
- Australia/Sydney
```
## Changing Themes & Colors
In your Rill project directory, create a `.yaml` file in any directory. Rill will automatically ingest the theme next time you run `rill start` or deploy to Rill Cloud and change the color scheme of your dashboard. All properties in the file are optional—any values you omit will fall back to Rill's standard theme defaults.
First, create the YAML file as below. You can define separate color schemes for light and dark modes:
```yaml
type: theme
light:
primary: "#4F46E5" # Indigo-600
secondary: "#8B5CF6" # Purple-500
variables:
# Sequential palette - for ordered data that progresses from low to high
color-sequential-1: "hsl(211deg 79% 94%)"
color-sequential-2: "hsl(211deg 63% 86%)"
color-sequential-3: "hsl(211deg 75% 77%)"
color-sequential-4: "hsl(210deg 73% 64%)"
color-sequential-5: "hsl(208deg 76% 51%)"
color-sequential-6: "hsl(210deg 100% 43%)"
color-sequential-7: "hsl(212deg 100% 36%)"
color-sequential-8: "hsl(214deg 100% 29%)"
color-sequential-9: "hsl(217deg 100% 22%)"
# Diverging palette - for data that diverges from a critical midpoint
color-diverging-1: "hsl(353deg 87% 48%)"
color-diverging-2: "hsl(12deg 100% 62%)"
color-diverging-3: "hsl(27deg 100% 70%)"
color-diverging-4: "hsl(40deg 96% 82%)"
color-diverging-5: "hsl(59deg 48% 94%)"
color-diverging-6: "hsl(194deg 100% 86%)"
color-diverging-7: "hsl(199deg 91% 73%)"
color-diverging-8: "hsl(202deg 83% 57%)"
color-diverging-9: "hsl(207deg 100% 44%)"
color-diverging-10: "hsl(217deg 100% 39%)"
color-diverging-11: "hsl(237deg 69% 34%)"
# Qualitative palette - for categorical data (showing first 12 of 24)
color-qualitative-1: "hsl(156deg 56% 52%)"
color-qualitative-2: "hsl(27deg 100% 65%)"
color-qualitative-3: "hsl(195deg 100% 46%)"
color-qualitative-4: "hsl(289deg 61% 76%)"
color-qualitative-5: "hsl(109deg 56% 64%)"
color-qualitative-6: "hsl(41deg 83% 69%)"
color-qualitative-7: "hsl(349deg 76% 71%)"
color-qualitative-8: "hsl(217deg 49% 61%)"
color-qualitative-9: "hsl(165deg 100% 36%)"
color-qualitative-10: "hsl(16deg 95% 70%)"
color-qualitative-11: "hsl(236deg 65% 74%)"
color-qualitative-12: "hsl(75deg 43% 66%)"
# ... up to color-qualitative-24
dark:
primary: "#818CF8" # Indigo-400
secondary: "#A78BFA" # Purple-400
variables:
# Sequential palette - adjusted for dark backgrounds
color-sequential-1: "hsl(210deg 20% 25%)"
color-sequential-2: "hsl(210deg 25% 30%)"
color-sequential-3: "hsl(210deg 30% 35%)"
color-sequential-4: "hsl(210deg 35% 40%)"
color-sequential-5: "hsl(210deg 40% 45%)"
color-sequential-6: "hsl(210deg 45% 50%)"
color-sequential-7: "hsl(210deg 50% 55%)"
color-sequential-8: "hsl(210deg 55% 60%)"
color-sequential-9: "hsl(210deg 60% 65%)"
# ... (diverging and qualitative palettes also available)
```
The `light` and `dark` properties allow you to customize:
- **primary**: Primary color used for charts, buttons, and interactive elements
- **secondary**: Secondary color (used for loading spinners and accents)
- **variables**: Custom CSS variables for complete control over color palettes
- **Sequential palette** (color-sequential-1 through 9): For ordered data progressions
- **Diverging palette** (color-diverging-1 through 11): For data diverging from a midpoint
- **Qualitative palette** (color-qualitative-1 through 24): For categorical data
Colors can be specified using:
- Hex values (with or without the '#' character, e.g., `4F46E5`. `"#4F46E5"`)
- Named colors (e.g., `plum`, `violet`)
- HSL format (e.g., `hsl(180, 100%, 50%)`)
Once you have created that file, update the `dashboard.yaml` with the following configuration (we typically add this at the top along with time zones, time series and other configurations):
`theme: `
:::info Theme properties
For more details about configuring themes, you can refer to our [Theme YAML](/reference/project-files/themes) reference page.
:::
## Example
```yaml
# Explore YAML
# Reference documentation: https://docs.rilldata.com/reference/project-files/explores
type: explore
display_name: "Programmatic Ads Auction"
metrics_view: auction_metrics
dimensions:
expr: "*"
measures:
- requests
- avg_bid_floor
- 1d_qps
defaults:
measures:
- avg_bid_floor
- requests
dimensions:
- app_site_cat
- app_site_domain
- app_site_name
- pub_name
comparison_mode: time
time_range: P7D
time_ranges:
- rill-TD
- rill-WTD
- rill-MTD
- rill-QTD
- rill-YTD
- rill-PDC
- rill-PWC
- rill-PMC
- rill-PQC
- rill-PYC
theme:
light:
primary: "#14B8A6" # Teal-500
secondary: "#10B981" # Emerald-500
variables:
color-sequential-1: "hsl(180deg 80% 95%)"
color-sequential-5: "hsl(180deg 80% 50%)"
color-sequential-9: "hsl(180deg 80% 25%)"
dark:
primary: "#2DD4BF" # Teal-400
secondary: "#34D399" # Emerald-400
security:
access: "{{ .user.admin }} OR '{{ .user.domain }}' == 'example.com'" # only access can be set on dashboard level, see metric view for detailed access policies '{{ .user.domain }}' == 'example.com'" # only access can be set on dashboard level, see metric view for detailed access policies
```
---
###### Dashboards in Rill
---
title: Dashboards in Rill
description: Understanding Explore vs Canvas dashboards and when to use each
sidebar_label: Overview
sidebar_position: 00
---
Rill offers two distinct dashboard experiences, each optimized for different use cases and user needs. While both are used to visualize your data, the key difference lies in how they handle [**metrics views**](/developers/build/metrics-view) - the data sources that power your dashboards.
- **Explore Dashboards** - Standardized _opinionated_ dashboards with consistent data components, visualizations, and layout structure that promote slice-and-dice discovery and interactive data exploration. These are built over a **single metrics view** using the [One Big Table approach](/developers/build/models/models-101#one-big-table-and-dashboarding).
- **Canvas Dashboards** - Traditional dashboards that allow users to customize visualizations and layout according to their specific requirements utilizing **multiple metrics views**.
## Explore Dashboards
**[Explore dashboards](/developers/build/dashboards/explore)** are designed for interactive data exploration and analysis. They provide a powerful "slice and dice" experience focused on a single metrics view. Some key features of our Explore dashboards include:
- [Pivot Tables](/guide/dashboards/explore/pivot)
- [Time Dimension Detail](/guide/dashboards/explore/tdd)
- [Leaderboards](/guide/dashboards/explore/multi-metrics)

### Live Demo
See both dashboard types in action with our interactive demos:
- **[Programmatic Advertising Explore Dashboard](https://ui.rilldata.com/demo/rill-openrtb-prog-ads/explore/auction_explore?tr=-24h+to+ref+as+of+latest%2FD&tz=UTC&grain=day&measures=1d_qps%2Cavg_bid_floor%2Crequests&dims=pub_name%2Capp_site_name%2Cad_size%2Capp_site_domain%2Cdevice_osv&leaderboard_measures=1d_qps%2Cavg_bid_floor%2Crequests)** - Real-time bidding and auction data exploration
- **[Mobile App Engagement Explore Dashboard](https://ui.rilldata.com/demo/rill-app-engagement/explore/mobile_events_explore)** - Mobile app user behavior and engagement metrics
- **[GitHub Analytics Explore Dashboard](https://ui.rilldata.com/demo/rill-github-analytics/explore/rill_commits_explore?tr=P12M&grain=week&dims=username%2Cfile_path%2Cfilename%2Cfile_extension%2Cfirst_directory%2Csecond_directory%2Cprevious_file_path%2Cis_merge_commit)** - Code repository and commit analysis
- **[E-commerce Explore Dashboard](https://ui.rilldata.com/demo/ezcommerce-demo/explore/data_metrics_explore)** - Interactive exploration of sales data
## Canvas Dashboards
**[Canvas dashboards](/developers/build/dashboards/canvas)** provide a traditional dashboard experience where you can combine multiple metrics views and create custom layouts. See our [list of components](/developers/build/dashboards/canvas-widgets) to start creating your custom dashboard.

### Live Demo
See both dashboard types in action with our interactive demos:
- **[E-commerce demo dashboard](https://ui.rilldata.com/demo/ezcommerce-demo/canvas/canvas)** - Sales analytics and revenue tracking with multiple visualizations
- **[Programmatic advertising demo dashboard](https://ui.rilldata.com/demo/rill-openrtb-prog-ads/canvas/executive_overview)** - Real-time bidding metrics and campaign performance analysis
- **[New York City demo dashboard](https://ui.rilldata.com/demo/nyc-canvas-jam/canvas/scorecard%20canvas)** - Urban analytics and city performance metrics
- **[NYC party demo dashboard 🎉](https://ui.rilldata.com/demo/nyc-canvas-jam/canvas/Leaderboard)** - Fun leaderboard showcasing various data insights
## Key Features
Both dashboard types share common capabilities that make them powerful for data analysis:
- **Time filtering and comparisons** - Navigate through time periods and compare different time ranges
- **Dimension filtering** - Filter data by specific dimensions and values
## Learn More About Using Dashboards
For comprehensive guidance on using dashboards as an analyst, see our [Analyst's Guide to Dashboards](/guide/dashboards/explore), which covers:
- [Dashboard navigation and interface](/guide/dashboards/explore)
- [Filtering and comparisons](/guide/dashboards/filters)
- [Pivot tables](/guide/dashboards/explore/pivot)
- [Time dimension details](/guide/dashboards/explore/tdd)
- [Bookmarks and sharing](/guide/dashboards/bookmarks)
- [Alerts and notifications](/guide/alerts)
---
###### Create Dashboards
---
title: Create Dashboards
description: Create and customize interactive dashboards for data exploration and analysis
sidebar_position: 00
---
Rill provides powerful dashboard capabilities to visualize and explore your data.
- [**Dashboard Overview**](/developers/build/dashboards/dashboards-101) - Compare Explore vs Canvas dashboards and choose the right one for your needs
- [**Create an Explore Dashboard**](/developers/build/dashboards/explore) - Interactive, slice-and-dice interfaces for dynamic data exploration and analysis. Built on top of a single metrics view, they provide real-time filtering, drilling, and comparison capabilities.
- [**Create a Canvas Dashboard**](/developers/build/dashboards/canvas) - Traditional dashboards that can be built on multiple metrics views, allowing you to consolidate key measures in a single view.
- [**Canvas Widgets**](/developers/build/dashboards/canvas-widgets) - Browse all available widgets
- [**Customization & Themes**](/developers/build/dashboards/customization) - Customize your dashboard's available dimensions and measures, and create default views for your viewers.
---
###### Explore Dashboards
---
title: Explore Dashboards
description: Explore dashboard overview
sidebar_label: Explore Dashboards
sidebar_position: 00
---
Explore dashboards are interactive, slice-and-dice interfaces that transform your metrics view data into powerful visualizations for data exploration and analysis. Built on top of a single metrics view, they provide an intuitive way for users to interact with your data through real-time filtering, drilling, and comparison capabilities.
## Creating an Explore Dashboard in Rill Developer
### Using the Code Editor
In the Explore dashboard YAML, you can define dashboard level parameters to customize the capabilities. For a full list, see our [explore dashboard reference](/reference/project-files/explore-dashboards) doc.
* _**metrics_view**_ - A single metrics view that powers the dashboard
* _**measures**_ - `*` Which measures to include or exclude from the metrics view; using a wildcard will include all.
* _**dimensions**_ - `*` Which dimensions to include or exclude from the metrics view; using a wildcard will include all.
In some cases, a specific dashboard will not need to include all of the underlying metrics view's measures and/or dimensions. In this case, you can use the `measures` and `dimensions` parameters to filter these out. Rill supports providing a single value, list, or regex to filter out unnecessary measures and dimensions.
```yaml
type: explore
title: Title of your Explore Dashboard
description: a description for your explore dashboard
metrics_view: my_metricsview
dimensions:
expr: "^public_.*$"
measures:
- total_downloads
- total_impressions
defaults:
comparison_mode: time
time_range: P3M
measures:
- total_downloads
- total_impressions
dimensions:
- show_name
- season
- program_name
```
:::tip Customize default time ranges
Set project-wide default time ranges and available options for all explore dashboards.
[Learn more about dashboard defaults →](/developers/build/project-configuration#explore-defaults)
:::
### Using AI
In various locations throughout the platform, you have the opportunity to fast-track your dashboard creation via AI. This feature [creates the underlying metrics view with AI](/developers/build/metrics-view/what-are-metrics-views#creating-a-metrics-view-with-ai) and generates your explore.yaml similar to the example above with the required components.
**Directly from your connector's tables:**

**From an ingested model:**

## Preview a Dashboard in Rill Developer
Once a dashboard is ready to preview, before [deploying to Rill Cloud](/developers/deploy/deploy-dashboard), you can preview the dashboard in Rill Developer.

### Setting Up Dashboard Access
If you are setting up [dashboard policies](/developers/build/dashboards/customization#define-dashboard-access), it is recommended to preview and test the dashboard before deploying. This option will be available for testing if you have set up access policies at the [project level](/developers/build/project-configuration#testing-security), [metrics view level](/developers/build/metrics-view/security), or [dashboard level](/developers/build/dashboards/customization#define-dashboard-access).

---
#### Debugging
---
###### "Debugging Rill Projects"
---
title: "Debugging Rill Projects"
description: "Tools and techniques for debugging Rill projects"
sidebar_label: "Debugging"
sidebar_position: 30
---
When building Rill projects, you'll encounter various debugging scenarios—from understanding project logs to tracing resource reconciliation. This section covers the tools and techniques available for troubleshooting your Rill projects.
- **[Understanding Project Logs](#understanding-project-logs)** - Learn the basics of reading and interpreting logs
- **[Troubleshooting Common Errors](#troubleshooting-common-errors)** - Resolve common error patterns
- **[Advanced Debugging Techniques](#advanced-debugging-techniques)** - Use debug flags, trace viewer, and cloud logs
## Understanding Project Logs
Whether you start Rill from the terminal or your favorite IDE, the terminal window will output the project logs. From reconciling items to partition ingestion and beyond, browsing the project logs is a great place to start when troubleshooting errors or slow-loading models.
### Log Format
Rill logs follow a structured JSON format. Here are some common log entries:
```bash
Reconciled resource {"name": "commits__ (copy)_metrics_explore", "type": "Explore", "elapsed": "1ms"}
Executed model partition {"model": "CH_incremental_commits_directory", "key": "55454ed4ad31cd3266988fe523103637", "data": {"path":"github-analytics/Clickhouse/2025/08","uri":"gs://rilldata-public/github-analytics/Clickhouse/2025/08"}, "elapsed": "283.188333ms"}
Executed model partition {"model": "staging_to_CH", "key": "0030406e528b3799c8cbad6bfe609e83", "trace_id": "3073a89ac5cee9e7e3433ce0a34d291a", "span_id": "c3cb402d7b4af9b6", "data": {"day":"2022-12-20T00:00:00Z"}}
```
### Generic Logging
- **`name`** – Filename or YAML-defined name of the Rill object.
- **`type`** – Resource type (e.g., `Connector`, `Model`, `MetricsView`, `Explore`, `API`, `Alert`, `Theme`, `Component`, `Canvas`).
- **`elapsed`** – Time taken to reconcile, execute, or otherwise process the resource.
- **`error`** – Error message generated during reconciliation or execution.
- **`dependency_error`** – Boolean flag indicating that the resource failed due to another resource's error.
- **`deleted`** – Boolean flag indicating that the resource was deleted.
- **`path`** – Filesystem path to the resource YAML file.
- **`logger_name`** – Name of the logger emitting the message.
- **`message`** – Log message content.
### Partitioning
- **`model`** – Name of the model associated with a partition operation.
- **`partitions`** – Number of partitions resolved for a model.
- **`key`** – Partition key (usually an MD5-like hash).
- **`data`** – Partition-specific metadata or parameters.
### Embedded ClickHouse
- **`addr`** – Host and port address for the embedded ClickHouse server.
- **`sql`** – SQL statement executed by ClickHouse during query execution.
### Debug
- **`sql`** – SQL statement executed by DuckDB during model evaluation or metrics computation.
- **`args`** – SQL query parameters (if any).
- **`trace_id`** – Unique trace identifier for the operation (used for distributed tracing).
- **`span_id`** – Unique span identifier within the trace.
### Logging Examples
#### Simple: Project Creation
When you first initialize a Rill project, you'll see Rill reconcile a resource "duckdb" of type "Connector". This is expected as we explicitly create this file to initialize a connection to our embedded DuckDB.
```bash
Rill will create project files in "~/Desktop/GitHub/testing-folder/dsn". Do you want to continue? Yes
INFO Serving Rill on: http://localhost:9009
INFO Reconciling resource {"name": "duckdb", "type": "Connector"}
INFO Reconciled resource {"name": "duckdb", "type": "Connector", "elapsed": "90ms"}
```
#### Simple: Connecting to a Data Source
When connecting to a data source via a connector, you'll see a "Connector" being reconciled. In the case of any errors, you'll see this in both the UI and the logs.
```bash
INFO Reconciling resource {"name": "gcs", "type": "Connector"}
INFO Reconciled resource {"name": "gcs", "type": "Connector", "elapsed": "39ms"}
WARN Reconcile failed {"name": "gcs", "type": "Connector", "elapsed": "1ms", "error": "failed to resolve templated property \"google_application_credentials\": template: :1:6: executing \"\" at <.env.GOOGLE_APPLICATION_CREDENTIALS>: map has no entry for key \"GOOGLE_APPLICATION_CREDENTIALS\""}
```
Once connected, you'll likely create a model and see this also reconciling in the logs. Similar to the above, if there are any issues, you'll see it both in the logs and UI.
```bash
INFO Reconciling resource {"name": "commits__", "type": "Model"}
INFO Reconciled resource {"name": "commits__", "type": "Model", "elapsed": "944ms"}
# or
WARN Reconcile failed {"name": "commits__", "type": "Model", "elapsed": "682ms", "error": "blob (key \"github-analytics/Clickhouse/2025/06/commits_2025_0.parquet\") (code=Unknown): storage: object doesn't exist: googleapi: Error 404: No such object: rilldata-public/github-analytics/Clickhouse/2025/06/commits_2025_0.parquet, notFound", "errorVerbose": "blob (key \"github-analytics/Clickhouse/2025/06/commits_2025_0.parquet\") (code=Unknown):\n gocloud.dev/blob.(*Bucket).Attributes\n /Users/runner/go/pkg/mod/gocloud.dev@v0.36.0/blob/blob.go:913\n - storage: object doesn't exist: googleapi: Error 404: No such object: rilldata-public/github-analytics/Clickhouse/2025/06/commits_2025_0.parquet, notFound"}
```
#### Intermediate: Creating Rill Objects
The next section of logs shows the creation of a metrics view and explore dashboard. You'll see some errors thrown in the metrics view that get resolved in Rill Developer.
```bash
INFO Reconciling resource {"name": "commits___metrics", "type": "MetricsView"}
WARN Reconcile failed {"name": "commits___metrics", "type": "MetricsView", "elapsed": "15ms", "error": "measure \"earliest_commit_date_measure\" is of type CODE_TIMESTAMP, but must be a numeric type\nmeasure \"latest_commit_date_measure\" is of type CODE_TIMESTAMP, but must be a numeric type"}
INFO Reconciling resource {"name": "commits___metrics", "type": "MetricsView"}
WARN Reconcile failed {"name": "commits___metrics", "type": "MetricsView", "elapsed": "21ms", "error": "measure \"earliest_commit_date_measure\" is of type CODE_TIMESTAMP, but must be a numeric type"}
INFO Reconciling resource {"name": "commits___metrics", "type": "MetricsView"}
INFO Reconciled resource {"name": "commits___metrics", "type": "MetricsView", "elapsed": "15ms"}
INFO Reconciling resource {"name": "commits___metrics_explore", "type": "Explore"}
INFO Reconciled resource {"name": "commits___metrics_explore", "type": "Explore", "elapsed": "1ms"}
```
#### Advanced: Dependency Errors
When a resource fails, dependent resources will also fail with a `dependency_error` flag. This helps you trace the root cause of cascading failures. In the example below, the `orders` model fails, which causes `orders_customers_model` to fail with a dependency error, which in turn causes `orders_customers_metrics` and `orders_customers_explore` to fail.
```bash
INFO Reconciling resource {"name": "duckdb", "type": "Connector"}
INFO Reconciling resource {"name": "gcs", "type": "Connector"}
INFO Reconciling resource {"name": "orders", "type": "Model"}
INFO Reconciling resource {"name": "customers", "type": "Model"}
INFO Reconciled resource {"name": "gcs", "type": "Connector", "elapsed": "1ms"}
INFO Reconciled resource {"name": "duckdb", "type": "Connector", "elapsed": "96ms"}
INFO Reconciled resource {"name": "customers", "type": "Model", "elapsed": "10.8s"}
WARN Reconcile failed {"name": "orders", "type": "Model", "elapsed": "17.5s", "error": "failed to create model: Cannot open file \"/path/to/project/tmp/default/duckdb/orders/data.db.wal\": No such file or directory"}
INFO Reconciling resource {"name": "orders_customers_model", "type": "Model"}
INFO Reconciled resource {"name": "orders_customers_model", "type": "Model", "error": "dependency error: resource \"orders\" (rill.runtime.v1.Model) has an error", "dependency_error": true}
INFO Reconciling resource {"name": "orders_customers_metrics", "type": "MetricsView"}
WARN Reconcile failed {"name": "orders_customers_metrics", "type": "MetricsView", "elapsed": "11ms", "error": "table \"orders_customers_model\" does not exist"}
INFO Reconciling resource {"name": "orders_customers_explore", "type": "Explore"}
INFO Reconciled resource {"name": "orders_customers_explore", "type": "Explore", "error": "dependency error: resource \"orders_customers_metrics\" (rill.runtime.v1.MetricsView) has an error", "dependency_error": true}
```
#### Advanced: Partitioned Models
The main takeaway for partitioned models is that you'll be able to see the number of partitions that Rill will start ingesting. This is especially important when creating [dev/prod](/developers/build/connectors/templating) environments and you're trying to avoid ingesting large amounts of data locally.
```bash
Resolved model partitions {"model": "staging_to_CH", "partitions": 16}
INFO Executed model partition {"model": "staging_to_CH", "key": "0030406e528b3799c8cbad6bfe609e83", "data": {"day":"2022-12-20T00:00:00Z"}}
```
## Troubleshooting Common Errors
When debugging errors, start by checking the project logs and understanding the error messages. Here are common error patterns and how to resolve them:
### Model Errors
Model errors typically occur when there are issues with credentials, data processing, SQL syntax, or data type mismatches. Common error messages and their solutions:
- **`Failed to connect to ...`**: Issue with your connector. Check your credentials and [firewall settings](/developers/build/connectors/data-source#externally-hosted-services) if using externally hosted services
- **`Table with name ... does not exist!`**: Verify the table exists by running `rill query --sql "select * from {table_name} limit 1"` or checking your data source
- **`IO Error: No files found that match the pattern...`**: Check that your cloud storage folder path is correct and files exist
- **`some partitions have errors`**: Run `rill project refresh --model {model_name} --errored-partitions` to refresh errored partitions
- **`Out of Memory Error: ...`**: Contact [support](/contact) for assistance with memory issues
### Metrics View and Dashboard Errors
Metrics view and dashboard errors often stem from issues with the underlying models or configuration problems:
- **Model Dependencies:** Dashboards failing because their underlying models have errors. Check the [dependency errors](/developers/build/debugging#model-errors) section above
- **Missing Dimensions/Measures:** References to fields that don't exist in the underlying model. Verify that measures and dimensions in your metrics YAML match existing columns in your data
- **Type Mismatches:** Measures must be numeric types. Check that timestamp fields aren't being used as measures
### Checking Resource Status
To understand what's failing in your project:
1. **Check project logs** - Review the terminal output or use `rill project logs` for Rill Cloud projects
2. **Use the Trace Viewer** - Visualize resource reconciliation and trace execution paths
3. **Check resource status** - Use the `Status` tab in Rill Developer or [`rill project status`](/reference/cli/project/status) CLI command
:::tip Check upstream dependencies
The surfaced error might not be the root cause. A dashboard error could stem from an underlying model timeout. Always trace errors to their source by checking dependent resources.
:::
## Advanced Debugging Techniques
When standard logs aren't providing enough detail, Rill offers several advanced debugging options to help you diagnose issues more effectively.
### Using Debug and Verbose Flags
Rill Developer provides two flags for increasing log verbosity:
**`--verbose`**: Sets the log level to debug, showing more detailed information about what Rill is doing internally. This includes:
- More granular resource reconciliation steps
- Additional context about operations
- Extended error details
**`--debug`**: Collects additional debug information beyond just log verbosity. This flag enables:
- Enhanced debugging metadata
- More detailed trace information
- Additional diagnostic data useful for troubleshooting complex issues
```bash
# Increase log verbosity
rill start --verbose
# Collect additional debug info
rill start --debug
# Combine both for maximum detail
rill start --debug --verbose
```
:::tip When to use each flag
- Use `--verbose` when you need more detail about what Rill is doing but don't need deep debugging info
- Use `--debug` when troubleshooting complex issues that require additional diagnostic data
- Use both together when you need the most comprehensive debugging information
:::
### Log Format Options
By default, Rill outputs logs in a human-readable console format. For programmatic processing or filtering, you can output logs in JSON format:
```bash
rill start --log-format json
```
JSON format is useful when:
- Parsing logs with scripts or tools
- Filtering logs programmatically
- Integrating with log aggregation systems
### Viewing Rill Cloud Logs
For projects deployed to Rill Cloud, you can view logs directly from the CLI:
```bash
# View recent logs
rill project logs
# Follow logs in real-time (like tail -f)
rill project logs --follow
# Show only the last N lines
rill project logs --tail 100
# Filter by log level
rill project logs --level DEBUG
```
The `rill project logs` command provides the same structured log output you see in Rill Developer, making it easy to debug issues in production deployments.
### Checking Project Status
Use the `rill project status` command to get a quick overview of your project's health:
```bash
# Check status of a deployed project
rill project status
# Check status of locally running project
rill project status --local
```
This command shows:
- Resource reconciliation status
- Error states for individual resources
- Dependency relationships
- Overall project health
### Trace Viewer
For complex debugging scenarios involving multiple resources and dependencies, use the [Trace Viewer](/developers/build/debugging/trace-viewer) to visualize resource reconciliation and trace execution paths across your project. The Trace Viewer helps you:
- Understand resource dependency chains
- Identify bottlenecks in reconciliation
- Visualize execution flows
- Debug cascading failures
To use the Trace Viewer, start Rill with the `--debug` flag:
```bash
rill start --debug
```
Then access the Trace Viewer through the Rill Developer UI to see a visual representation of your project's resource reconciliation.
---
###### "Trace Viewer in Rill Developer"
---
title: "Trace Viewer in Rill Developer"
description: Visualize resource reconciliation and trace execution
sidebar_label: "Trace Viewer in Rill Developer"
sidebar_position: 00
---
Rill Developer provides a built-in trace viewer, enabling users to visually inspect operations performed when reconciling resources or fetching data for dashboards. This helps in diagnosing performance and operational issues.

## How to Use the Trace Viewer
### Step 1: Start Rill Developer with Debug Mode
Launch Rill Developer with the debug flag enabled:
```bash
rill start --debug
```
### Step 2: Access the Trace Viewer
Open your web browser and navigate to:
```
http://localhost:9009/traces
```
### Step 3: Visualize Traces
There are two ways to visualize traces:
- **By Trace ID:** Enter a specific Trace ID to inspect a particular operation.
- **By Resource Reconciliation:** Browse operations associated with reconciling specific resources.
For example, to view the operations performed during the reconciliation of the `bids` model, enter `bids` in the resource name field.
## Understanding the Trace Graph
- **Horizontal Bars:** Each horizontal bar represents a distinct operation performed by Rill.
- **Bar Length:** The length of each bar indicates the duration of the operation.
- **Operation Details:** Click on any bar to view detailed tags and metadata in the pane on the right.
- **Nested Operations:** Operations may trigger other sub-operations, displayed as nested bars beneath the parent operation.
- **Parallel Operations:** Bars displayed side by side indicate operations executed concurrently.
## Retrieving the Trace ID for Dashboard Data Fetch
To find the Trace ID associated with fetching data for a dashboard:
1. **Open Browser Developer Tools:** Use your browser's developer tools (usually `F12` or `Cmd+Option+I`).
2. **Inspect API Requests:** Select the API call you're interested in from the network tab.
3. **Find `X-trace-id`:** Check the response headers to locate the `X-trace-id`.
4. **Use Trace ID:** Enter this Trace ID into the trace viewer to inspect the operation details.

## Inner Workings
- The Trace Viewer UI uses OpenTelemetry (OTEL) traces.
- Traces are captured in JSON format and stored locally.
- The UI retrieves and filters traces based on query parameters and trace type.
---
#### Ide
---
###### Build Rill Projects with Your Favorite IDE
---
title: Build Rill Projects with Your Favorite IDE
description: Use VS Code, IntelliJ, or any IDE to create and edit Rill projects with real-time feedback
sidebar_label: External IDE Integration
sidebar_position: 00
---
## Use Any IDE for Rill Development
Rill projects are just files and folders that you can edit with any code editor or IDE. Whether you prefer VS Code, IntelliJ, Vim, or any other editor, you can create and modify Rill projects directly from your favorite development environment.
### How It Works
Rill projects consist of:
- **SQL files** (`.sql`) for models and metrics views
- **YAML files** (`.yml`) for project configuration
- **Data files** in various formats
You can edit these files in any IDE, and Rill will automatically detect changes and provide real-time feedback.

## Using AI Agents to Build Rill Projects
Rill ships built-in instructions that teach AI coding agents like **Claude Code** and **Cursor** how to build Rill projects. A single `rill init` command scaffolds everything your agent needs — resource schemas, best practices, and development conventions.
```bash
# Add Claude Code instructions to your project
rill init --template claude
# Or add Cursor rules
rill init --template cursor
```
---
#### Metrics View
---
##### Dimensions
---
####### "Clickable Dimension Links"
---
title: "Clickable Dimension Links"
description: "Make dimension values clickable by adding URI parameters to create interactive links in your dashboards"
sidebar_label: "Clickable Dimension Links"
sidebar_position: 50
---
You can make dimension values clickable by adding a `uri` parameter to your dimension configuration. This enables users to click directly on dimension values in the dashboard to navigate to external URLs, making your dashboards more interactive and useful for data exploration.

## Simple Setup: Column Already Contains URLs
If your column values are already URLs, simply add `uri: true` to the dimension:
```yaml
dimensions:
- display_name: Company URL
column: company_url
uri: true
```
## Advanced Setup: Dynamic URL Generation
For more advanced use cases, you can dynamically create URLs using expressions. The dimension displays the generated URL and uses it as the clickable link:
```yaml
dimensions:
- name: profile_url
display_name: Bluesky Profile Link
expression: CONCAT('https://bsky.app/profile/', profile_id)
uri: true
```
---
####### "Define Your Dimensions"
---
title: "Define Your Dimensions"
description: "Learn how to create and configure dimensions for data segmentation, filtering, and analysis in your metrics views"
---
Dimensions are fundamental components of metrics views that enable data segmentation, filtering, and analysis. They represent categorical or descriptive attributes of your data that allow you to break down measures into meaningful segments.
Dimensions are the "who," "what,", and "where" of your data. They provide context and categorization for your measures, enabling you to:
- **Segment data**: Break down metrics by categories like region, product type, or customer segment
- **Filter analysis**: Focus on specific subsets of your data
- **Enable drill-downs**: Navigate from high-level summaries to detailed breakdowns
- **Create comparisons**: Analyze performance across different groups
Explore these advanced capabilities to enhance your dimensions:
- [**Time Dimensions**](/developers/build/metrics-view/dimensions/time-dimensions) - Add multiple time-based dimensions for filtering across different date columns
- [**Clickable Dimension URI**](/developers/build/metrics-view/dimensions/dimension-uri) - Generate navigable URIs for your dimension values
- [**Unnesting Arrays**](/developers/build/metrics-view/dimensions/unnesting) - Work with array data by expanding nested structures
- [**Lookup Dimensions**](/developers/build/metrics-view/dimensions/lookup) - Create dimensions that reference data from other sources
---
####### "Query-Time Dimension Joins"
---
title: "Query-Time Dimension Joins"
description: "Use lookup dimensions to enrich your metrics view data with reference information at query time"
sidebar_label: "Query-Time Joins"
sidebar_position: 55
---
Query-time joins or more simply, lookup dimensions, provide a powerful way to incorporate reference tables into your dashboard without requiring upstream SQL transformations. This feature enables data enrichment with reference information at query time, eliminating the need for complex ETL processes.
:::note
The lookup function is dependent on the type of [OLAP engine](/developers/build/connectors/olap) that you are using for your project.
:::
## ClickHouse Lookups
ClickHouse provides powerful dictionary functions for lookup operations through `dictGet` functions. The table needs to be defined as a dictionary.
### Using `expression`
You can use `dictGet` directly in a dimension's `expression`:
```yaml
dimensions:
- name: user_email
display_name: "User Email"
# Query-time lookup in the 'users' dictionary
expression: dictGet('users', 'email', user_id)
description: "User email from users dictionary"
- name: product_category
display_name: "Product Category"
expression: dictGet('products', 'category', product_id)
description: "Product category information"
# With default fallback
- name: user_email_safe
display_name: "User Email (Safe)"
expression: dictGetOrDefault('users', 'email', user_id, 'unknown@example.com')
description: "User email with fallback for missing users"
# Multiple attributes from same dictionary
- name: user_full_name
display_name: "User Full Name"
expression: concat(dictGet('users', 'first_name', user_id), ' ', dictGet('users', 'last_name', user_id))
description: "Concatenated full name from user dictionary"
```
This approach is straightforward but has a performance drawback: when filtering by a lookup dimension, ClickHouse must evaluate `dictGet` for every row before applying the filter, which prevents it from using indexes on the key column.
### Using `lookup_*` properties
The `lookup_*` properties provide a declarative way to define lookup dimensions. Rill automatically rewrites filter queries to use an optimized subquery pattern, enabling ClickHouse to leverage indexes on key columns.
```yaml
dimensions:
- name: country_name
display_name: "Country"
# column in the fact table to match with the dictionary key
column: country_code
lookup_table: country_dict
lookup_key_column: code
lookup_value_column: name
# With a default value for missing keys
- name: region
display_name: "Region"
# column in the fact table to match with the dictionary key
column: country_code
lookup_table: country_dict
lookup_key_column: code
lookup_value_column: region
lookup_default_expression: "'Unknown'"
```
The properties are:
- **`lookup_table`** — the dictionary name. Use `database.dictionary_name` if the dictionary is in a non-default database (e.g. `mydb.my_dict`).
- **`lookup_key_column`** — the primary key column in the dictionary that matches the fact table's `column`.
- **`lookup_value_column`** — the attribute column in the dictionary to display.
- **`lookup_default_expression`** _(optional)_ — a SQL expression used as a fallback when no match is found (maps to `dictGetOrDefault`).
#### How the query optimization works
When you filter on a lookup dimension defined with `expression: dictGet(...)`, the generated query looks like:
```sql
SELECT ... FROM fact_table
WHERE dictGet('dict', 'name', publisher_id) IN ('Alice', 'Bob')
```
ClickHouse must call `dictGet` on every row to evaluate the filter, making it unable to use the index on `publisher_id`.
With `lookup_*` properties, Rill rewrites the filter to a subquery pattern:
```sql
SELECT ... FROM fact_table
WHERE publisher_id IN (
SELECT code FROM dict WHERE name IN ('Alice', 'Bob')
)
```
This allows ClickHouse to first resolve the small set of matching keys from the dictionary, then use the index on `publisher_id` to efficiently scan only the relevant rows in the fact table.
:::tip Optimizing GROUP BY with INJECTIVE
If the key-to-value mapping in your dictionary is 1:1 (e.g. every `code` maps to exactly one `name`), you can additionally mark the attribute columns as `INJECTIVE` in the ClickHouse dictionary definition. This tells ClickHouse that the mapping preserves uniqueness, allowing it to optimize `GROUP BY` queries by grouping on the key column instead of evaluating `dictGet` for every group. Combined with `lookup_*` properties, this gives you optimized performance for both filtering (via Rill's subquery rewrite) and grouping (via ClickHouse's INJECTIVE optimization).
:::
## Druid Lookups
For those looking to add ID to name mappings with Druid (as an OLAP engine), you can utilize expressions in your **Dimension** settings. Simply use the `lookup` function and provide the name of the lookup and ID, i.e. `lookup(city_id, 'cities')`. Be sure to include the lookup table name in single quotes.
```yaml
dimensions:
- name: city_name
display_name: "Cities"
expression: lookup(city_id, 'cities')
description: "City names from lookup table"
```
## DuckDB/MotherDuck Lookups
Unfortunately, DuckDB does not have a dimension lookup **function** and instead relies on upstream modeling to join the lookup table and create a [one big table](/developers/build/models/models-101#one-big-table-and-dashboarding) for dashboarding. Take a look through [DuckDB docs](https://duckdb.org/docs/stable/sql/introduction) for further information!
```sql
SELECT
o.*,
u.user_name
FROM orders o
LEFT JOIN users u ON u.email = o.email;
```
Alternatively, you can use DuckDB's `map` function to create a lookup table by mapping values from one column to another. This approach creates an in-memory mapping that can be referenced without using a joining SQL:
```yaml
- expression: (SELECT map(list(email), list(user_name)) FROM users_dataset)[email]
name: user_name
```
## Performance Implications
While joins can kill the performance of [OLAP engines](/developers/build/connectors/olap), lookups (key-value pairs) are common to reduce data size and improve query speeds. Lookups can be done during ingestion time (a static lookup to enrich the source data) or at query time (dynamic lookups).
**Static Lookups**
Static lookups are lookups that are ingested at processing time. When a record is being processed, if a match is found between the record and lookup's key, the lookup's corresponding value at that moment in time is extracted and carbon-copied into your OLAP engine for the records it processed.
Static lookups are best suited for:
- Dimensions with values that require a historical record for how they have changed over time
- Values that are never expected to change (leverage Dynamic Lookups if the values are expected to change)
- Extremely large lookups (hundreds of thousands of records or >50MB lookup file) to improve query performance
Customers typically store lookup values in S3 or GCS, and the lookup file is then updated by customers as needed and consumed by ETL logic.
**Dynamic Lookups**
Since static lookups transform and store the data permanently, any changes to the mapping would require reprocessing the entire dataset to ensure consistency. To address the case when values in a lookup are expected to change with time, we developed dynamic lookups. Dynamic lookups, also known as Query Time Lookups, are lookups that are retrieved at query time, as opposed to being used at ingestion time.
Benefits of dynamic lookups include:
- Historical continuity for dimensions that change frequently without reprocessing the entire dataset
- Time savings, because there is no dataset reprocessing required to complete the update
- Dynamic lookups are kept separate from the dataset. Thus, any human errors introduced in the lookup do not impact the underlying dataset
- Ability for users to create new dimension tables from metadata associated with a dimension table. For example, account ownership can change during the course of a quarter. In such cases, a dynamic lookup ensures that the most current ownership information is always available without requiring data reprocessing.
---
####### "Time Dimensions"
---
title: "Time Dimensions"
description: "Add multiple time dimensions to enable time-based filtering and analysis across different temporal columns"
sidebar_label: "Time Dimensions"
sidebar_position: 52
---
While every metrics view has a primary `timeseries` column that powers the main time series chart, you can define additional time dimensions using `type: time`. This enables users to filter and analyze data across multiple temporal columns.
## When to Use Time Dimensions
Time dimensions are useful when your data contains multiple date or timestamp columns that users may want to filter by:
- **Order date vs. Ship date**: Filter orders by when they were placed or when they were shipped
- **Created vs. Updated timestamps**: Analyze records by creation date or last modification
- **Event time vs. Processing time**: Distinguish between when events occurred and when they were recorded
- **Multiple business dates**: Handle scenarios like invoice date, due date, and payment date
## Adding a Time Dimension
To create a time dimension, add `type: time` to your dimension definition:
```yaml
version: 1
type: metrics_view
model: orders_model
timeseries: order_date # Primary time series for charts
dimensions:
- name: order_date
display_name: Order Date
column: order_date
type: time
- name: ship_date
display_name: Ship Date
column: ship_date
type: time
- name: customer_region
column: region
```
## Time Dimension vs. Timeseries
Understanding the difference between these two concepts is important:
| Feature | `timeseries` (top-level) | `type: time` (dimension) |
|---------|--------------------------|--------------------------|
| Purpose | Powers main time series chart | Enables time-based filtering |
| Chart display | Shows trends over time | Used in filter panel |
| Required | No (but recommended) | No |
| Multiple allowed | No (one per metrics view) | Yes |
The primary `timeseries` column determines which dates appear on the x-axis of your time series visualizations. Time dimensions provide additional temporal filtering options in the dashboard filter panel.
## Full Example
Here's a complete example with multiple time dimensions:
```yaml
version: 1
type: metrics_view
model: sales_model
timeseries: transaction_date
dimensions:
# Time dimensions
- name: transaction_date
display_name: Transaction Date
column: transaction_date
type: time
- name: fulfillment_date
display_name: Fulfillment Date
column: fulfillment_date
type: time
- name: return_date
display_name: Return Date
column: return_date
type: time
# Categorical dimensions
- name: product_category
display_name: Product Category
column: category
- name: store_location
display_name: Store Location
column: store_id
measures:
- name: total_sales
display_name: Total Sales
expression: SUM(amount)
- name: order_count
display_name: Order Count
expression: COUNT(*)
```
## Using Expressions
You can also create time dimensions using expressions to transform or derive time values:
```yaml
dimensions:
- name: order_month
display_name: Order Month
expression: DATE_TRUNC('month', order_date)
type: time
- name: fiscal_quarter_start
display_name: Fiscal Quarter Start
expression: DATE_TRUNC('quarter', order_date + INTERVAL '3 months') - INTERVAL '3 months'
type: time
```
:::tip
Time dimensions work with columns of type `TIMESTAMP`, `TIME`, or `DATE`. If your source data stores dates in a different format (like strings), use an expression to convert them to a proper date type.
:::
---
####### "Unnest Dimensions"
---
title: "Unnest Dimensions"
description: Tips & Tricks for Measure Formatting
sidebar_label: "Unnest Dimensions"
sidebar_position: 50
---
For multi-value fields, you can set the `unnest` property within the dimension. If `true`, this property allows a multi-valued dimension to be unnested (such as lists), and filters will automatically switch to "contains" instead of exact match.
## Example
In this example, the data contains an array column that has the value `['deal_one', 'deal_two', 'deal_three']`. Setting the unnest property enables the user to filter on each value in the array. Measures split by unnested values are non-additive, so that in this example the “Total Impressions” measure is applied equally across each value. Totals in Pivot Tables and the Time Dimension Detail view are calculated correctly, avoiding issues with double counted values when splitting multi-value dimensions.

```yaml
- display_name: "Deal Name"
column: deal_name
description: "Unnested Column"
unnest: true
```
In another example, we are provided with a directory_path column that gives us information on which path was edited. Using DuckDB's `regexp_split_to_array`, we have converted the string into an array. Using `unnest`, we can see which top directories are being updated more than others.

```yaml
- expression: regexp_split_to_array(directory_path, '/')
display_name: "The directory unnested"
description: "The directory path"
name: directory_path_unnested
unnest: true
```
## Demo
[See this project live in our demo!](https://ui.rilldata.com/demo/my-rill-tutorial/explore/advanced_explore?f=directory_path_unnested+IN+%28%27docs%27%29)
---
##### Measures
---
####### "Case Statements and Filters"
---
title: "Case Statements and Filters"
description: Tips & Tricks for Case Statements
sidebar_label: "Case Statements and Filters"
sidebar_position: 20
---
One of the most common advanced measure expressions is the use of [`case`](https://duckdb.org/docs/stable/sql/expressions/case.html) statements and [`filters`](https://duckdb.org/docs/stable/sql/query_syntax/filter.html) to filter or apply logic to part of the result set. Use cases for case statements include filtered sums (e.g., only sum if a flag is true) and bucketing data (e.g., if between threshold x and y, then apply an aggregate). While similar, case statements give you a bit more flexibility as they allow you to set a custom value depending on the case. See below for some examples!

Please review the reference documentation, [here.](/reference/project-files/metrics-views)
## Examples
### Case Statements
The following expression sums of the values of Global_active_power only when considered to be a lower value.
```yaml
- name: total_low_active_power_measure
display_name: Total Low Global Active Power
description: Total sum of Global Active Power where considered Low
expression: SUM(CASE WHEN GAP_category = 'Low' THEN Global_active_power ELSE 0 END)
format_preset: humanize
valid_percent_of_total: true
```
The following expression only considers the total value of users who are identified.
```yaml
- name: total_value_for_identified_users
display_name: Total Value for Identified Users
description: Total Sum of Value for Identified Users
expression: SUM(CASE WHEN user_id != '' OR user_id IS NOT NULL THEN value ELSE 0 END)
format_preset: humanize
valid_percent_of_total: true
```
The following expression modifies the value of the column based on the value of column XX
```yaml
- name: modify_value
display_name: Arithmetic on Value
description: Arithmetic on Value based on XX
expression: |
SUM(
CASE
WHEN XX = 'multiply_10' THEN Value * 10
WHEN XX = 'multiply_2' THEN Value * 2
WHEN XX = 'divide_5' THEN Value / 5
END
)
format_preset: humanize
valid_percent_of_total: true
```
### Filters
Similar to the above case statements, you can use the filter expression to filter the data on a specific column's value. However, in the example where we are explicitly changing the value in the CASE statement, this is not possible using only a filter.
```yaml
- name: total_low_active_power_measure
display_name: Total Low Global Active Power
description: Total sum of Global Active Power where considered Low
expression: sum(Global_active_power) FILTER (WHERE GAP_category = 'Low')
format_preset: humanize
valid_percent_of_total: true
```
```yaml
- name: total_value_for_identified_users
display_name: Total Value for Identified Users
description: Total Sum of Value for Identified Users
expression: SUM(value) FILTER (WHERE user_id != '' OR user_id IS NOT NULL)
format_preset: humanize
valid_percent_of_total: true
```
## Demo
[See this project live in our demo!](https://ui.rilldata.com/demo/rill-kaggle-elec-consumption/explore/household_power_consumption_metrics_explore)
---
####### "Fixed Measures"
---
title: "Fixed Measures"
description: Tips & Tricks for Fixed Measures
sidebar_label: "Fixed Measures"
sidebar_position: 35
---
Some measures may be at a different level of granularity where a sum across the measure is no longer accurate. As an example, perhaps you have a campaign with a daily budget of $5000 across five line items. Summing the `daily_budget` column would give an inaccurate total of $25,000 budget per day. For those familiar with Tableau, this is referred to as a `FIXED metric`.

To create the correct value, you can utilize DuckDB's unnest functionality. In the example below, you would be pulling a single value of `daily_budget` based on `campaign_id` to get the sum of budget for the day by campaign ids. Note that you can use multiple keys if your granularity is defined by multiple dimensions.
```yaml
expression: |
select
sum(a.val) as value
from
( select unnest(
list(distinct {
key: campaign_id,
val: daily_budget
})
) a
)
```
:::note
The syntax for fixed metrics is specific to DuckDB as an OLAP engine as it requires DuckDB-specific commands. However, you can create a similar SQL expression using a different OLAP engine, too!
:::
Please review the reference documentation, [here.](/reference/project-files/metrics-views)
## Example
In the following example, each publishing company has a monthly minimum guarantee. As you'll see in the measure, `incorrect_sum_of_guarantee`, you'll get an incorrect value as this will sum multiple values as there are multiple shows and days per publisher. Another workaround would be to use MIN, MAX, or AVG, but when selecting multiple publishers, the values will not be accurate.

```yaml
- name: incorrect_sum_of_guarantee
expression: sum(min_guarantee_usd)
format_preset: currency_usd
valid_percent_of_total: false
- name: guarantee_usd_measure_monthly
display_name: Monthly Minimum Guarantee USD
description: Total minimum guarantee in USD recorded in the dataset.
expression: >
SELECT SUM(a.val) AS value
FROM (
SELECT unnest(
list(distinct {
key: publisher_id,
month: date_trunc('month', date),
val: min_guarantee_usd
})
) a
)
format_preset: currency_usd
valid_percent_of_total: false
```
## Demo
[See this project live in our demo!](https://ui.rilldata.com/demo/sample-podcast-project/explore/podcast_explore)
---
####### "Formatting your Measures"
---
title: "Formatting your Measures"
description: Tips & Tricks for Measure Formatting
sidebar_label: "Measure Formatting"
sidebar_position: 15
---
When creating your measures in Rill, you have the option to pick from a preset of formats that we provide to you or use the [d3-format](https://d3js.org/d3-format) parameter to format your data in any way you like. While the big number in the explore dashboard won't apply all the decimals changes (it will add currency or percentage if that is the type), you will be able to see the changes in the dimension leaderboard and pivot tables.

Using `format_d3` to control the format of a measure in the metrics view allows for further customization.
:::tip Invalid format Strings
If an invalid format string is supplied, measures will be formatted with `format_preset: humanize`. Measures cannot have both `format_preset` and `format_d3` entries. If neither `format_preset` nor `format_d3` is supplied, measures will be formatted with the `humanize` preset.
:::
Please review the reference documentation, [here.](/reference/project-files/metrics-views)
## Customization
For further customization of your measures, you can switch to the YAML view and, with our [metrics view reference documentation](/reference/project-files/metrics-views), use the [format_d3_locale](https://d3js.org/d3-format#formatLocale) parameter to create specific formatting.
```yaml
format_d3:
format_d3_locale:
grouping:
currency:
```
## Examples
As explained in the introduction, you'll notice that in each of the screenshots the Big Number doesn't always follow the exact formatting, but will change based on percentage/currency formatting. This is as designed, as there is a fixed width that the number has to be displayed in. Instead, you'll see these values in the dimension leaderboard, TDD, and pivot tables.
If you have any questions, please review our [reference documentation.](/reference/project-files/metrics-views)
### Format a measure to include specific amount of decimals

In the case that you need to view more granular values of your data, you can set the decimal places to whatever value you need. In the above example, we are setting the average voltage measure to 4 decimal places to get a more accurate representation for each dimension.
```yaml
format_d3: ".4f"
```
### Format currency with different ',' locations. IE: Indian Rupee

```yaml
format_d3: "$,"
format_d3_locale:
grouping: [3, 2, 2]
currency: ["₹", ""]
```
As Indian Rupees are formatted in a different way than USD and EUR, you'll need to use the `format_d3_locale` parameter to set the exact grouping and currency. Likewise, if the currency symbol is written after the numeric value, you can set the currency to `["", "$"]`.
### Percentages

```yaml
format_d3: '.4%'
```
While our `format_preset: percentage` will automatically apply `.2%`, you can manually set the value in format_d3 if you are looking for a more specific measure format.
## Demo
[See this project live in our demo!](https://ui.rilldata.com/demo/rill-kaggle-elec-consumption/explore/household_power_consumption_metrics_explore)
---
####### "Define Your Measures"
---
title: "Define Your Measures"
description: "Learn how to create and configure measures for quantitative analysis and business insights in your metrics views"
---
Measures are the quantitative metrics that power your dashboards and reports. They represent numeric calculations derived from your underlying data through SQL aggregation functions and expressions. These measures transform raw data into meaningful business insights, such as total revenue, average order value, or customer count.
Measures are the "how much" and "how many" of your data. They provide the numerical foundation for your analysis, enabling you to:
- **Quantify performance**: Track key business metrics like revenue, growth, and efficiency
- **Support decision-making**: Provide concrete numbers for business decisions
## Supported SQL Functions
* Standard SQL [numeric](https://www.w3schools.com/sql/sql_operators.asp) operators and functions
* Common SQL [aggregates](https://www.w3schools.com/sql/sql_aggregate_functions.asp): `AVG`, `COUNT`, `MAX`, `MIN`, `SUM`, `STDDEV`, `VARIANCE`
* Advanced aggregates (engine-dependent): `APPROX_COUNT_DISTINCT`, `APPROX_QUANTILE`, `STDDEV_POP`, `STDDEV_SAMP`, `VAR_POP`, `VAR_SAMP`
* [Filtered aggregates](https://www.postgresql.org/docs/current/sql-expressions.html#SYNTAX-AGGREGATES) can be used to filter the set of rows fed to the aggregate functions (syntax may vary by engine)
:::info Engine-specific SQL dialects
Different OLAP engines support varying SQL dialects and functions. While standard SQL functions work across engines, some advanced features may be engine-specific. For engine-specific documentation, see:
- **DuckDB**: [DuckDB SQL documentation](https://duckdb.org/docs/sql/introduction.html)
- **ClickHouse**: [ClickHouse SQL documentation](https://clickhouse.com/docs/en/sql-reference)
- **Druid**: [Druid SQL documentation](https://druid.apache.org/docs/latest/querying/sql.html)
- **Pinot**: [Pinot SQL documentation](https://docs.pinot.apache.org/users/user-guide-query/pinot-query-language)
:::
As an example, if you have a table of sales events with the sales price and customer ID, you could calculate the following measures with these aggregates and expressions:
* Number of sales: `COUNT(*)`
* Total revenue: `SUM(sales_price)`
* Revenue per customer: `CAST(SUM(sales_price) AS FLOAT)/CAST(COUNT(DISTINCT customer_id) AS FLOAT)`
* Number of orders with order value more than $100: `COUNT(*) FILTER (WHERE order_val > 100)` (syntax may vary by engine)
Explore these advanced capabilities to enhance your measures:
- **[Measure Formatting](/developers/build/metrics-view/measures/measures-formatting)** - Learn how to format and display your measures effectively
- **[Case Statements and Filters](/developers/build/metrics-view/measures/case-statements)** - Use conditional logic and filtering in your measures
- **[Referencing Measures](/developers/build/metrics-view/measures/referencing)** - Reference and combine existing measures in your calculations
- **[Quantiles](/developers/build/metrics-view/measures/quantiles)** - Calculate percentiles and quantiles for statistical analysis
- **[Fixed Measures](/developers/build/metrics-view/measures/fixed-measures)** - Create measures with fixed values and constants
- **[Window Functions](/developers/build/metrics-view/measures/windows)** - Apply window functions for advanced analytical operations
---
####### "Quantiles"
---
title: "Quantiles"
description: Tips & Tricks for Measure Quantiles
sidebar_label: "Quantiles"
sidebar_position: 30
---
In addition to common aggregates, you may wish to look at the value of a measure within a certain band or quantile. In the example below, we can measure the P95 of a given measure using `QUANTILE_CONT`.

Using [DuckDB aggregate function](https://duckdb.org/docs/stable/sql/functions/aggregates.html#quantile_contx-pos), you can easily calculate various quantiles.
:::tip Not on DuckDB?
If you are using a different OLAP engine to power your dashboard, simply use the correct function for quantile calculation.
E.g.: [ClickHouse quantile](https://clickhouse.com/docs/sql-reference/aggregate-functions/reference/quantile), [Pinot percentile](https://docs.pinot.apache.org/configuration-reference/functions/percentile)
:::
Please review the reference documentation, [here.](/reference/project-files/metrics-views)
## Examples

In this example we see the values of P95 and P99 are calculated using the following expressions:
```yaml
- name: p95_quantile_global_intensity
expression: QUANTILE_CONT(Global_intensity, 0.95)
format_d3: ".3f"
description: P95 of Global Intensity
- name: p99_quantile_global_intensity
expression: QUANTILE_CONT(Global_intensity, 0.99)
format_d3: ".4f"
description: P99 of Global Intensity
```
## Demo
[See this project live in our demo!](https://ui.rilldata.com/demo/rill-kaggle-elec-consumption/explore/household_power_consumption_metrics_explore)
---
####### "Referencing Measures"
---
title: "Referencing Measures"
description: Tips & Tricks for Referencing Measure
sidebar_label: "Referencing Measures"
sidebar_position: 25
---
Within a metrics view, it is possible for a measure to reference another by using the `requires` array parameter. By doing this, you can easily aggregate already existing measures to simplify the expressions. For example, get a percentage of two already summed values.

Please review the reference documentation, [here.](/reference/project-files/metrics-views)
## Examples
### Simple Aggregation
In the following example, `percentage_reactive_to_active_measure` uses the already defined measures `total_global_active_power_measure` and `total_global_reactive_power_measure` to calculate the percentage without having to recalculate the sum of the respective columns.

```yaml
- name: percentage_reactive_to_active_measure
display_name: Percent Reactive to Active Power
requires: [total_global_active_power_measure, total_global_reactive_power_measure]
expression: total_global_reactive_power_measure / total_global_active_power_measure
format_preset: percentage
```
### Window Function
If you are using a [window function](./windows), you'll need to define the measure that you are building the window for. In this example, we are getting the rolling sum of average voltage measurements for all time— that's a lot of volts! You can modify the frame to include fewer rows based on the order column.

```yaml
- name: rolling_sum_avg_voltage_all_time
display_name: Rolling Sum Windowed Voltage Average
expression: SUM(average_voltage_measure)
requires: [average_voltage_measure]
window:
order: Date
frame: RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
treat_nulls_as: -1
```
## Demo
[See this project live in our demo!](https://ui.rilldata.com/demo/rill-kaggle-elec-consumption/explore/household_power_consumption_metrics_explore)
---
####### "Window Functions"
---
title: "Window Functions"
description: Tips & Tricks for Window Measures
sidebar_label: "Window Functions"
sidebar_position: 40
---
In addition to standard metrics, it is possible to define running window calculations on your data, whether you are looking to monitor a cumulative trend, smooth out fluctuations, etc. You'll need to navigate to the Code view in order to create a windowed measure.
Please review the reference documentation, [here.](/reference/project-files/metrics-views)
## Example
In the example below, bids is another measure defined in the metrics view, and we are getting the previous week and current date's values and averaging them. This allows us to remove any short-term trends to detect real patterns. You'll need to add the [`requires`](./referencing) array parameter to reference another measure.

```yaml
- name: bids_7day_rolling_avg
display_name: 7 Day Bid rolling avg
expression: AVG(total_bids)
requires: [total_bids]
window:
order: "__time"
frame: RANGE BETWEEN INTERVAL 6 DAY PRECEDING AND CURRENT ROW
```
Another example is using a rolling sum with no bounding preceding rows, also known as your whole data. This will be a cumulative sum of all of your measure's data; in this case, it is the average voltage measure.

```yaml
- name: rolling_sum_avg_voltage_all_time
display_name: Rolling Sum Windowed Voltage Average
expression: SUM(average_voltage_measure)
requires: [average_voltage_measure]
window:
order: Date
frame: RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
```
## Demo
[See this project live in our demo!](https://ui.rilldata.com/demo/rill-kaggle-elec-consumption/explore/household_power_consumption_metrics_explore)
---
##### Time Series
---
####### Time Series in Metrics Views
---
title: Time Series in Metrics Views
description: "Configure time-based dimensions and aggregations for comprehensive temporal analysis"
sidebar_label: Time Series
sidebar_position: 07
---
Time is the most critical dimension in analytics and powers our dashboards. Understanding not just the "what," but how metrics evolve over hours, days, and months provides the narrative arc for decision-making.
## Defining Your Time Series Column
Your time series must be a column from your data model of type `TIMESTAMP`, `TIME`, or `DATE`. If your source has a date in a different format, you can apply time functions to transform these fields into valid time series types. The specific functions available depend on your OLAP engine.
```yaml
# Metrics View YAML
# Reference documentation: https://docs.rilldata.com/reference/project-files/metrics_views
version: 1
type: metrics_view
model: example_model # Choose a table to underpin your metrics
timeseries: timestamp_column # Choose a timestamp column from your table
```
### Time Configuration Parameters
You can customize time behavior using the following parameters:
**`first_day_of_week`**
Specifies which day should be considered the start of the week. Valid values are 1 through 7, where Monday=1 and Sunday=7.
**`first_month_of_year`**
Determines which month should be treated as the beginning of the year. Valid values are 1 through 12, where January=1 and December=12.
```yaml
timeseries: transaction_date
first_month_of_year: 7 # July start
first_day_of_week: 2 # Monday start
```
These parameters enable you to define non-standard reporting periods. For example, if you set June as the starting month, "past year" calculations will span from June to May instead of January to December.
:::tip Full YAML Configurations
Please refer to our [reference page](/reference/project-files/metrics-views) for all the available parameters to define in a metrics view.
:::
### Time Grain Configuration
The `smallest_time_grain` parameter controls the minimum temporal resolution available in your dashboards. Limiting granularity provides several benefits:
- **Performance**: Reduces query complexity and improves dashboard responsiveness
- **Consistency**: Ensures all users see data at the same level of detail
- **Focus**: Prevents analysis paralysis from overly granular data
```yaml
timeseries: order_timestamp
smallest_time_grain: day
```
:::note Time Grain Availability
The time picker automatically adjusts available time grains based on your selected time range. For example, when viewing "Last 24 hours," only day, and hour grains are available, while "Last 30 days" offers day, week, and month grains. This ensures meaningful time-based analysis appropriate to your data range.
:::
### `watermark`
The `watermark` parameter defines the data freshness threshold for your metrics view. It determines the latest point in time where data is considered "complete" and reliable for analysis.
**Purpose:**
- Prevents analysis of incomplete or partial data
- Ensures consistent reporting across different time zones
- Provides a clear boundary between complete and incomplete data
**Configuration:**
```yaml
timeseries: event_time
watermark: "MAX(__TIME) - INTERVAL 3 DAYS"
```
**How it works:**
- `MAX(__TIME)` gets the latest timestamp in your data
- `INTERVAL 3 DAYS` subtracts 3 days from that timestamp
- If your latest data is September 5th, complete data extends only to September 2nd
- Queries for September 3rd-5th data will return empty or incomplete results
- This prevents misleading metrics from partial data
**Common watermark expressions:**
- `"MAX(__TIME) - INTERVAL 1 DAY"` - For daily batch processing (most common)
- `"MAX(__TIME) - INTERVAL 1 HOUR"` - For real-time data with hourly completeness
- `"MAX(__TIME) - INTERVAL 1 WEEK"` - For weekly aggregated data
- `"MAX(__TIME)"` - No watermark (use with caution)
:::tip Best practices
Set your watermark based on your data pipeline's processing time. If your ETL takes 2 hours to complete daily data, set watermark to "2 hours" or "1 day" to ensure you only analyze complete datasets.
:::
---
###### Annotations
---
title: Annotations
description: Enrich your metrics view with external context using annotations
sidebar_label: Annotations
sidebar_position: 20
---
Annotations allow you to enrich your metrics view with external data sources or context that isn't directly part of the primary model. This is often used to overlay events, holidays, or deployment markers onto your time series charts.

## Requirements
The underlying table or model used for annotations must strictly follow this schema:
1. **`time` column (Required)**: Used to position the annotation on the time series chart.
2. **`description` column (Required)**: The text displayed when hovering over the annotation.
3. **`time_end` column (Optional)**: Timestamp - If present, defines the end time for a range-based annotation.
4. **`duration` column (Optional)**: String/Enum - If present, defines the granularity (e.g., 'day', 'hour', 'minute').selected dashboard time grain. It also forces `time` and `time_end` in the UI to be truncated to the selected grain.
## Configuring Annotations
To add an annotation, you need to define a reference to a `table` or `model` in your metrics view YAML.
```yaml
annotations:
- model: annotations_auction
name: auction_annotations
measures: ['requests']
```
### Configuration Properties
- **`model`** or **`table`**: Reference to the data source containing the annotation data.
- **`name`**: A unique identifier for the annotation set.
- **`measures`** (optional): A list of measures to display these annotations alongside. If not specified, the annotation will appear for all measures.
### Visual Appearance
Annotations appear as markers or ranges on the time series charts in your dashboard.
- **Point Annotations**: Events with a single `time` timestamp appear as point markers.
- **Range Annotations**: Events with both `time` and `time_end` timestamps appear as shaded regions spanning the duration.
- **Hover Details**: Hovering over an annotation marker reveals the text from the `description` column.
:::info
Refer to the [`annotations` section](/reference/project-files/metrics-views#annotations) in Metrics View YAML reference for more details on how to implement annotations.
:::
---
###### Derived Metrics Views
---
title: Derived Metrics Views
description: Create derived metrics views that inherit dimensions, measures, and properties from a parent metrics view
sidebar_label: Derived Metrics Views
sidebar_position: 06
---
A derived metrics view inherits its dimensions, measures, and data source configuration from an existing parent metrics view. It can only expose a subset of the parent's dimensions and measures — it cannot define new ones. This lets you create focused views for specific teams or use cases without duplicating definitions. The parent metrics view should contain the superset of all dimensions and measures that any derived view might need.
## Basic Example
Given a parent metrics view `ad_bids.yaml`:
```yaml
version: 1
type: metrics_view
model: ad_bids_model
timeseries: timestamp
smallest_time_grain: hour
dimensions:
- column: domain
- column: city
- column: country
- column: device
measures:
- name: total_bids
expression: count(*)
- name: total_revenue
expression: sum(bid_price)
- name: avg_bid
expression: avg(bid_price)
```
A derived metrics view `ad_bids_summary.yaml` that inherits everything:
```yaml
type: metrics_view
parent: ad_bids
```
This creates a metrics view with the same dimensions, measures, timeseries, and data source as `ad_bids`. An explore dashboard is automatically emitted for it.
## Selecting Dimensions and Measures
Use `parent_dimensions` and `parent_measures` to control which fields are inherited. When omitted, all fields are inherited (equivalent to `'*'`).
### Select All (Wildcard)
```yaml
type: metrics_view
parent: ad_bids
parent_dimensions: '*'
parent_measures: '*'
```
### Select by Name
```yaml
type: metrics_view
parent: ad_bids
parent_dimensions:
- domain
- country
parent_measures:
- total_bids
- total_revenue
```
### Exclude Specific Fields
Use `exclude` to inherit everything except specific fields:
```yaml
type: metrics_view
parent: ad_bids
parent_dimensions:
exclude:
- city
parent_measures:
exclude:
- avg_bid
```
### Select by Regex
```yaml
type: metrics_view
parent: ad_bids
parent_dimensions:
regex: "^(domain|country)$"
parent_measures:
regex: "^total_.*"
```
### Select by DuckDB Expression
```yaml
type: metrics_view
parent: ad_bids
parent_dimensions:
expr: "* EXCLUDE (city)"
```
## Overriding Inherited Properties
A derived metrics view can override certain properties from the parent. Properties you set on the child take precedence; properties you omit are inherited.
### Overridable Properties
| Property | Behavior |
|---|---|
| `display_name` | Child's value if set, otherwise defaults to the resource name (not inherited from parent) |
| `description` | Child's value if set, otherwise empty (not inherited from parent) |
| `timeseries` | Child's value if set, otherwise parent's |
| `smallest_time_grain` | Child's value if set, otherwise parent's. Must be >= parent's grain |
| `first_day_of_week` | Child's value if set, otherwise parent's |
| `first_month_of_year` | Child's value if set, otherwise parent's |
| `watermark` | Child's value if set, otherwise parent's |
| `ai_instructions` | Child's value if set, otherwise parent's |
### Always Inherited (Cannot Override)
These properties are always taken from the parent:
- `model` / `table` (data source)
- `connector`, `database`, `database_schema`
- Cache settings (`cache.enabled`, `cache.key_sql`, `cache.key_ttl`)
### Example: Override Time Grain
The parent uses hourly data, but this derived view restricts to daily granularity:
```yaml
type: metrics_view
parent: ad_bids
display_name: Ad Bids Daily Summary
smallest_time_grain: day
parent_dimensions:
- domain
- country
parent_measures: '*'
```
:::info
When overriding `smallest_time_grain`, the value must be equal to or coarser than the parent's grain. For example, if the parent uses `hour`, the child can use `day` but not `minute`.
:::
## Dimensions and Measures
A derived metrics view can only inherit dimensions and measures from its parent — it cannot define its own `dimensions` or `measures` directly. The parent metrics view must contain the superset of all dimensions and measures that any derived view might need. Use `parent_dimensions` and `parent_measures` to select which subset to expose.
:::caution
Setting `dimensions` or `measures` on a derived metrics view will produce a validation error. All dimensions and measures must be defined on the parent.
:::
## Security Rules
All security rules from the parent are inherited by the derived metrics view. The only exception is the `access` rule: if the derived view defines its own `access` rule, the parent's `access` rule is skipped. All other parent rules (`row_filter`, `field_access`, `include`, `exclude`) are always appended to the child's rules regardless.
This means a derived view can narrow who has access (by overriding `access`), and can add additional row filters or field restrictions on top of whatever the parent already enforces.
```yaml
type: metrics_view
parent: ad_bids
parent_dimensions: '*'
parent_measures: '*'
security:
access: "'{{ .user.domain }}' = 'partner.com'"
row_filter: "country = 'US'"
```
In this example, the child overrides the parent's `access` rule with its own, but the parent's `row_filter`, `include`, `exclude`, and `field_access` rules are still inherited and appended to the child's rules. The child also adds its own `row_filter` on top of the parent's.
## Inline Explore Configuration
By default, a derived metrics view automatically emits an explore dashboard. You can customize it using the `explore` key, or disable it entirely.
:::note
Auto-emission of the explore dashboard only happens when the YAML file does not set `version` (or sets `version: 0`). If `version: 1` is set, no explore is emitted automatically — you must either define one inline with the `explore` key or create a separate explore file.
:::
### Default Behavior
When no `explore` key is specified and `version` is not set, an explore dashboard is created with all inherited dimensions and measures.
### Custom Explore
```yaml
type: metrics_view
parent: ad_bids
parent_dimensions: '*'
parent_measures: '*'
explore:
display_name: Ad Bids Partner View
time_ranges:
- P7D
- P30D
- range: P90D
comparison_offsets:
- P90D
defaults:
dimensions:
- domain
measures:
- total_bids
- total_revenue
time_range: P7D
comparison_mode: time
```
### Disable Explore
```yaml
type: metrics_view
parent: ad_bids
parent_dimensions: '*'
parent_measures: '*'
explore:
skip: true
```
### Explore Options
The inline `explore` key supports the same configuration as a standalone [explore dashboard](/reference/project-files/explore-dashboards):
| Property | Description |
|---|---|
| `skip` | Set to `true` to disable the explore dashboard |
| `name` | Custom name for the explore resource |
| `display_name` | Display name shown in the UI |
| `description` | Description for the explore |
| `banner` | Custom banner at the header of the explore |
| `theme` | Theme name or inline theme object |
| `time_ranges` | Available time range selections |
| `time_zones` | Pinned time zones (IANA identifiers) |
| `lock_time_zone` | Lock to the first time zone in `time_zones` |
| `allow_custom_time_range` | Allow custom time range selection (default: `true`) |
| `defaults` | Default UI state: `dimensions`, `measures`, `time_range`, `comparison_mode`, `comparison_dimension` |
| `embeds` | Embed configuration, e.g. `hide_pivot: true` |
## Complete Example
A parent metrics view and two derived views for different teams:
**`metrics/ad_bids.yaml`** — the shared parent:
```yaml
version: 1
type: metrics_view
model: ad_bids_model
timeseries: timestamp
smallest_time_grain: hour
dimensions:
- column: domain
- column: city
- column: country
- column: device
- column: publisher
measures:
- name: total_bids
expression: count(*)
- name: total_revenue
expression: sum(bid_price)
- name: avg_bid
expression: avg(bid_price)
- name: unique_domains
expression: count(distinct domain)
security:
access: true
row_filter: "bid_price > 0"
```
**`metrics/ad_bids_sales.yaml`** — for the sales team:
```yaml
type: metrics_view
parent: ad_bids
display_name: Ad Bids - Sales
parent_dimensions:
- domain
- country
parent_measures:
- total_revenue
- avg_bid
smallest_time_grain: day
explore:
display_name: Sales Dashboard
defaults:
time_range: P30D
measures:
- total_revenue
```
**`metrics/ad_bids_engineering.yaml`** — for the engineering team:
```yaml
type: metrics_view
parent: ad_bids
display_name: Ad Bids - Engineering
parent_dimensions: '*'
parent_measures:
exclude:
- avg_bid
security:
access: "'{{ .user.department }}' = 'engineering'"
explore:
defaults:
time_range: P7D
comparison_mode: time
```
## Validation Rules
The parser enforces these constraints on derived metrics views:
- `parent` must reference an existing metrics view with a valid state.
- `dimensions` and `measures` cannot be defined on a derived view — use `parent_dimensions` and `parent_measures` to select from the parent instead.
- `model`, `table`, `database`, `database_schema`, and cache settings cannot be set on a derived view (they come from the parent).
- `parent_dimensions` and `parent_measures` can only be used when `parent` is set.
- `smallest_time_grain`, if specified, must be coarser than or equal to the parent's grain.
- Deprecated top-level explore fields (`default_time_range`, `available_time_zones`, etc.) cannot be used; use the `explore` key instead.
---
###### Metrics SQL
---
title: Metrics SQL
description: Query metrics views using SQL syntax
sidebar_label: Metrics SQL
---
You can write a SQL query referring to metrics definitions and dimensions defined in a metrics view.
It should have the following structure:
```yaml
type: api
metrics_sql: SELECT publisher, domain, total_records FROM ad_bids_metrics
```
## Querying Fundamentals
Metrics SQL transforms queries that reference `dimensions` and `measures` within a `metrics view` into their corresponding database columns or expressions. This transformation is based on the mappings defined in a metrics view YAML configuration, enabling reuse of dimension or measure definitions. Additionally, any security policies defined in the metrics view are also inherited.
## Example: Crafting a Metrics SQL Query
Consider a metrics view configured as follows:
```yaml
#metrics/ad_bids_metrics.yaml
type: metrics_view
title: Ad Bids
model: ad_bids
timeseries: timestamp
dimensions:
- name: publisher
expression: toUpper(publisher)
- name: domain
column: domain
measures:
- name: total_records
display_name: Total records
expression: COUNT(*)
```
To query this view, a user might write a Metrics SQL query like:
```sql
SELECT publisher, domain, total_records FROM ad_bids_metrics
```
This Metrics SQL is internally translated to a standard SQL query as follows:
```sql
SELECT toUpper(publisher) AS publisher, domain AS domain, COUNT(*) AS total_records FROM ad_bids_metrics GROUP BY publisher, domain
```
## Security and Compliance
Queries executed via Metrics SQL are subject to the security policies and access controls defined in the metrics view YAML configuration, ensuring data security and compliance.
## Supported SQL Features
### SELECT
Reference dimensions and measures by name. The `date_trunc` function can be used to group a time dimension by a specific grain (and optionally aliased with `AS`):
```sql
SELECT date_trunc('MONTH', timestamp) AS month, publisher, total_records FROM ad_bids_metrics
```
Supported grains: `SECOND`, `MINUTE`, `HOUR`, `DAY`, `WEEK`, `MONTH`, `QUARTER`, `YEAR`.
### FROM
A single metrics view name. Joins and subqueries in the FROM clause are not supported.
### WHERE and HAVING
`WHERE` filters on dimensions; `HAVING` filters on aggregated measures. Both support the same operators and functions.
### ORDER BY, LIMIT, OFFSET
Standard SQL sorting and pagination clauses are supported:
```sql
SELECT publisher, total_records FROM ad_bids_metrics
ORDER BY total_records DESC
LIMIT 20 OFFSET 40
```
## Operators
The following operators are supported in `WHERE` and `HAVING` clauses:
| Operator |
|----------|
| `=`, `!=`, `<`, `<=`, `>`, `>=` |
| `AND`, `OR`, `(` `)` |
| `IN (...)`, `NOT IN (...)` |
| `LIKE`, `NOT LIKE` (case-insensitive, `%` wildcard) |
| `BETWEEN ... AND ...` |
| `IS NULL`, `IS NOT NULL` |
| `IS TRUE`, `IS FALSE`, `IS NOT TRUE`, `IS NOT FALSE` |
```sql
SELECT publisher, total_records FROM ad_bids_metrics
WHERE (publisher IS NOT NULL AND domain LIKE '%google%')
OR publisher IN ('Yahoo', 'Microsoft')
```
## Functions
### Time range functions
`time_range_start` and `time_range_end` resolve a Rill time expression against the metrics view's watermark and time range. They must be compared against the time dimension:
```sql
SELECT publisher, total_records FROM ad_bids_metrics
WHERE timestamp > time_range_start('7D as of watermark/D+1D')
AND timestamp <= time_range_end('7D as of watermark/D+1D')
```
### Interval arithmetic
Add or subtract an interval from a timestamp literal using `INTERVAL amount UNIT` syntax. Supported units: `SECOND`, `MINUTE`, `HOUR`, `DAY`, `WEEK`, `MONTH`, `YEAR`.
```sql
SELECT publisher, total_records FROM ad_bids_metrics
WHERE timestamp > '2024-07-30' - INTERVAL 90 DAY
```
### now()
Returns the current timestamp:
```sql
SELECT publisher, total_records FROM ad_bids_metrics
WHERE timestamp > now() - INTERVAL 7 DAY
```
### CAST
Casting to `DATETIME` or `TIMESTAMP` is supported (other target types are not):
```sql
SELECT publisher, total_records FROM ad_bids_metrics
WHERE timestamp > CAST('2024-01-01' AS TIMESTAMP)
```
## Subqueries
Subqueries are supported inside `IN` expressions. The subquery must select exactly one dimension from the same metrics view and can include its own `WHERE` and `HAVING` clauses:
```sql
SELECT publisher, total_records FROM ad_bids_metrics
WHERE publisher IN (
SELECT publisher FROM ad_bids_metrics
HAVING total_records > 100
)
```
Subqueries do not support `ORDER BY`, `LIMIT`, `DISTINCT`, window functions, joins, or CTEs.
## Limitations
- Only one metrics view can be queried per statement (no joins).
- `SELECT *` is not supported; list dimensions and measures explicitly.
- `GROUP BY` is implicit based on selected dimensions and cannot be specified manually.
- Aggregate functions like `COUNT()` or `SUM()` cannot be used directly; reference predefined measures instead.
- Set operations (`UNION`, `INTERSECT`, `EXCEPT`) and CTEs (`WITH`) are not supported.
:::warning
The Metrics SQL feature is currently evolving. We are dedicated to enhancing the syntax by introducing additional SQL features, while striving to maintain support for existing syntax. However, please be advised that backward compatibility cannot be guaranteed at all times. Additionally, users should be aware that there may be untested edge cases in the current implementation. We appreciate your understanding as we work to refine and improve this feature.
:::
## Using Metrics SQL in custom APIs
To expose Metrics SQL queries as HTTP API endpoints, see the [Metrics SQL APIs](/developers/build/custom-apis/metrics-sql) guide. You can also add [dynamic templating](/developers/build/custom-apis/templating), [security rules](/developers/build/custom-apis/security), and [OpenAPI documentation](/developers/build/custom-apis/openapi) to your Metrics SQL APIs.
---
###### Create Metrics Views
---
title: Create Metrics Views
description: Create metrics views using source data and models with time, dimensions, and measures
sidebar_label: Create Metrics Views
sidebar_position: 00
---
Metrics Views are a key part of defining your measures and dimensions.
### Metrics 101
- [**What are Metrics Views?**](/developers/build/metrics-view/what-are-metrics-views) - Define your time series, measures, and dimensions using your OLAP engine aggregate functions
- [**Underlying Model/Table**](/developers/build/metrics-view/underlying-model) - When to use `model` or `table` in a metrics view
- [**Time Series**](/developers/build/metrics-view/time-series) - What is a time series column and how to use it in your metrics view
- [**Derived Metrics Views**](/developers/build/metrics-view/derived-metrics-views) - Create focused subsets by inheriting from a parent metrics view
### Dimensions
- [**Dimensions**](/developers/build/metrics-view/dimensions) - Categorize and segment your data for analysis and filtering
- [**Navigatable Dimensions**](/developers/build/metrics-view/dimensions/dimension-uri) - Generate navigable URIs for your dimension values
- [**Unnesting Arrays**](/developers/build/metrics-view/dimensions/unnesting) - Work with array data by expanding nested structures
- [**Time Dimensions**](/developers/build/metrics-view/dimensions/time-dimensions) - Add multiple time-based dimensions for filtering across different date columns
- [**Lookup Dimensions**](/developers/build/metrics-view/dimensions/lookup) - Create dimensions that reference data from other sources
### Measures
- [**Measures**](/developers/build/metrics-view/measures) - Define calculated metrics and aggregations for your data analysis
- [**Format Measures**](/developers/build/metrics-view/measures/measures-formatting) - Learn how to format and display your metrics effectively
- [**Case Statements and Filters**](/developers/build/metrics-view/measures/case-statements) - Use conditional logic and filtering in your measures
- [**Referencing Measures**](/developers/build/metrics-view/measures/referencing) - Reference and combine existing measures in your calculations
- [**Quantiles**](/developers/build/metrics-view/measures/quantiles) - Calculate percentiles and quantiles for statistical analysis
- [**Fixed Measures**](/developers/build/metrics-view/measures/fixed-measures) - Create measures with fixed values and constants
- [**Window Functions**](/developers/build/metrics-view/measures/windows) - Apply window functions for advanced analytical operations
### Performance
- [**Rollups**](/developers/build/metrics-view/rollups) - Accelerate queries by routing to pre-aggregated rollup tables
### Security
- [**Data Access Policies**](/developers/build/metrics-view/security) - Control access to your metrics data with role-based permissions
---
###### Rollups
---
title: Rollups
description: Accelerate metrics view queries by routing to pre-aggregated rollup tables
sidebar_label: Rollups
sidebar_position: 25
---
Rollups let a metrics view be backed by one or more pre-aggregated tables in addition to the base table. When a query's time grain, dimensions, measures, time range, and filters all match a rollup, Rill transparently rewrites the query to read from the rollup table instead of the base.
## Defining a Rollup
A rollup is defined as a separate table in the external olap engine or as a model that pre-aggregates the base model, plus a `rollups` entry in the metrics view YAML that points to it. The rollup model must produce columns with the same names as the base model's time dimension, the dimensions you list, and the measure expression inputs.
The simplest case — a daily rollup of an hourly fact table:
```yaml
# metrics_views/events.yaml
type: metrics_view
version: 1
model: events
timeseries: timestamp
dimensions:
- name: publisher
column: publisher
- name: domain
column: domain
- name: country
column: country
measures:
- name: total_impressions
expression: SUM("impressions")
- name: total_clicks
expression: SUM("clicks")
rollups:
- model: events_daily
time_grain: day
dimensions: [publisher, domain]
measures: [total_impressions, total_clicks]
```
Note that the rollup omits `country`. Queries that group by or filter on `country` will fall back to the base table; queries on `publisher` and/or `domain` at day grain or coarser will use the rollup.
### Multiple Rollups
You can define several rollups at different grains. Rill will pick the most efficient one that can answer a given query:
```yaml
rollups:
- model: events_daily
time_grain: day
dimensions: [publisher, domain]
measures: [total_impressions, total_clicks]
- model: events_monthly
time_grain: month
dimensions: [publisher, domain]
measures: [total_impressions, total_clicks]
```
A query at month grain over the full year will be served from `events_monthly`; a query at day grain over a single month will be served from `events_daily`.
### Field Selectors
`dimensions` and `measures` accept the standard field-selector forms — an explicit list, a wildcard, a regex, or an exclusion, if not defined then all dimensions and measures are included. For example, this rollup includes all dimensions and all measures except `total_clicks` measure:
```yaml
rollups:
- model: events_daily
time_grain: day
measures:
exclude: [total_clicks] # all measures except total_clicks
```
### Declaring Coverage with `data_time_range`
By default Rill discovers a rollup's time coverage at query time by running `SELECT min(time), max(time)` against the rollup table. If you'd rather declare coverage statically — to skip the probe, or to scope a rollup to a specific window — set `data_time_range` on the rollup. The value is a [rilltime expression](/reference/time-syntax):
```yaml
rollups:
- model: events_hourly
time_grain: hour
data_time_range: -1Y to now # rolling last 12 months
dimensions: [publisher, domain]
measures: [total_impressions]
- model: events_daily_archive
time_grain: day
data_time_range: -5Y to -1Y # 1 to 5 years ago
dimensions: [publisher, domain]
measures: [total_impressions]
```
A query for last week routes to `events_hourly`; a query 18 months back routes to `events_daily_archive`; a query reaching past 5 years falls back to the base.
You can declare coverage on the metrics view itself the same way — this skips the probe on the base table:
```yaml
data_time_range: -5Y to now
```
When `data_time_range` is set, the rilltime expression is resolved against fixed anchors: `now`/`latest`/`watermark` all resolve to the current wallclock. The start must be bounded — `inf` and `earliest` are rejected, because they resolve start time to the zero time value, which the system treats as "no data". To declare full historical coverage, either omit `data_time_range` so the bounds are probed from the table, or use a concrete early bound such as `-100Y to now`. Mixing declared and undeclared rollups in the same metrics view is fine — each table independently decides whether to probe or to use its declaration.
### Selection Priority and Definition Order
When two rollups have the same time grain and both could answer a query, the **first one declared in the YAML wins**. This is the lever for priority-style routing: list narrower / fewer-row rollups first.
```yaml
rollups:
# Same grain, different dimension sets. Priority is declared by order.
- model: events_daily_narrow # selected for queries that only need publisher
time_grain: day
dimensions: [publisher]
measures: [total_impressions]
- model: events_daily_wide # selected when publisher + domain are queried
time_grain: day
dimensions: [publisher, domain]
measures: [total_impressions]
- model: events_daily_wider # selected when publisher + domain + country are queried
time_grain: day
dimensions: [publisher, domain, country]
measures: [total_impressions]
```
A query on `publisher` alone is eligible against all three — `events_daily_narrow` wins because it's listed first. A query on `publisher` + `domain` knocks `events_daily_narrow` out of eligibility, so `events_daily_wide` wins.
### Configuration Reference
- **`model`** (required) — The pre-aggregated table or model.
- **`time_grain`** (required) — Grain of the rollup. One of `millisecond`, `second`, `minute`, `hour`, `day`, `week`, `month`, `quarter`, `year`.
- **`time_zone`** (optional) — IANA timezone the rollup was bucketed in (e.g. `America/New_York`). For day and coarser grains, queries are routed to the rollup only if their timezone matches.
- **`database`**, **`database_schema`** (optional) — Override the OLAP database and schema for the rollup table.
- **`dimensions`** (optional) — Field selector for which base-view dimensions are present in the rollup. Defaults to all.
- **`measures`** (optional) — Field selector for which base-view measures are present in the rollup. Defaults to all.
- **`data_time_range`** (optional) — Rilltime expression describing the rollup's time coverage. When set, Rill skips the OLAP `min/max` probe for this rollup and uses the declared bounds for coverage checks.
A metrics view must define a `timeseries` to use rollups. The metrics view itself also accepts a top-level `data_time_range` to declare the base table's coverage. The full schema is documented in the [metrics view reference](/reference/project-files/metrics-views#rollups).
## How Rollup Selection Works
For each query, Rill walks through three phases: a quick disqualification, a per-rollup eligibility check, and a selection step among the eligible rollups.
### 1. Quick Disqualification
The whole rollup system is skipped — and the base table is used — when:
- The query asks for raw rows rather than aggregates.
- The query has a comparison time range. (Time comparison queries always read the base table.)
- The query's time range is on a time dimension that isn't the metrics view's primary `timeseries`.
### 2. Eligibility
A rollup is eligible for a given query only if **all** of the following hold:
1. **Grain derivable.** The query's time grain can be aggregated up from the rollup's grain. For example, a `month` query can be derived from a `day` rollup, but a `week` query cannot be derived from a `month` rollup, and a `month` query cannot be derived from a `week` rollup. Sub-day grains form one chain (`ms → s → min → hour → day`); calendar grains form another (`day → month → quarter → year`); `week` sits on its own branch and can only be derived from `day` or finer.
2. **Timezone matches** (day grain and coarser). The query's timezone must equal the rollup's `time_zone`. UTC variants (`""`, `"UTC"`, `"Etc/UTC"`) are treated as equivalent. Sub-day grains are timezone-agnostic, so this check is skipped for them.
3. **Start aligned.** The query's time-range start must fall exactly on a rollup-grain boundary in the rollup's timezone. A query starting at `2024-01-01 12:00` cannot use a daily rollup, because the noon boundary doesn't line up with a day bucket.
4. **All queried dimensions present.** Every dimension in the query (group-by, time floor, or WHERE filter) must be in the rollup's `dimensions` list. The primary time dimension is always considered present.
5. **All queried measures present.** Every measure named in the query must be in the rollup's `measures` list. Computed measures like `COUNT(*)` or `COUNT(DISTINCT …)` are rejected outright — they would produce wrong results when applied on top of pre-aggregated rows.
### 3. Time Coverage
For each eligible rollup, Rill checks that the rollup actually contains data for the requested range:
- **With a time range.** The query range is first clamped to the base table's `[min, max]` (so a query that extends past the base data isn't penalized for the rollup also stopping there). The rollup must then cover the clamped start and end.
- **Without a time range** ("all data"). The rollup must cover the base table's full `[min, max]`.
- **End alignment.** If the base table has data beyond the query's end time, the end must also be aligned to the rollup grain. Otherwise the last rollup bucket would pull in data from outside the requested range. Queries whose end falls past the latest base data don't need to be end-aligned, because there is no extra data to pull in.
### 4. Selection
Among rollups that pass eligibility and coverage:
1. Prefer the **coarsest grain** — fewer rows to scan.
2. On a tie, prefer the rollup **declared earlier** in the `rollups` list — this is the lever for explicit priority among same-grain rollups.
The base table is used if no rollup is eligible.
## Limitations and Edge Cases
- **Time grain must be derivable.** `week` and the calendar grains (`month`, `quarter`, `year`) live on separate branches. A weekly rollup cannot answer monthly queries, and a monthly rollup cannot answer weekly queries. Define rollups at the grain you actually query.
- **Day-and-above rollups are timezone-specific.** A rollup bucketed in UTC cannot serve a dashboard query in `America/New_York`, because the day boundaries are different. If your users query in multiple timezones, either materialize a rollup per timezone or keep the rollup at hour grain (which is timezone-agnostic).
- **Misaligned starts disqualify the rollup.** A query starting mid-bucket (e.g. `2024-01-01 12:00` against a daily rollup) silently falls back to the base table. Dashboard time-range pickers typically snap to grain boundaries; ad-hoc API queries may not.
- **Computed measures fall back.** `count` and `count_distinct` measures bypass rollups even if the rollup looks otherwise suitable, because counting pre-aggregated rows is not the same as counting raw rows. Define an explicit `SUM(...)` measure on a pre-aggregated counter column in the rollup if you want this case to route.
- **Derived measures fall back.** A measure of type `derived` (one with `requires` or `per`) cannot match a rollup's measure list — only `simple` measures can. The base table is used.
- **Rollups require a `timeseries`.** Metrics views without a primary time dimension cannot define rollups.
- **Filters on missing dimensions disqualify the rollup.** A WHERE clause on `country` will skip a rollup that doesn't include `country`, even if the query's group-by columns are all in the rollup.
- **The rollup is responsible for being correct.** Rill does not validate that the rollup's measure values are consistent with the base — it trusts the model. If the rollup model uses the wrong aggregation (e.g. `AVG` where the base measure is `SUM`), queries routed to it will return wrong numbers.
- **Rollups are assumed to be roughly caught up with the base table.** Coverage is measured against the base table's latest timestamp. A rollup that lags behind the base will be silently skipped for any query that reaches the tail of the data — including common "last 24 hours" queries and queries without a time range — even if it has the right grain, dimensions, and measures. Refresh rollups in step with the base model so selection actually happens.
- **Rollups must not extend beyond the base table.** Routing assumes the rollup's max timestamp is no later than the base's. A rollup that gets ahead of the base (e.g. ingested through a separate path) may return incorrect results at the tail of the data.
:::info
The full configuration schema is in the [metrics view reference](/reference/project-files/metrics-views#rollups).
:::
---
###### Who Can Access Your Data
---
title: Who Can Access Your Data
description: Control who can view your metrics and data
sidebar_label: Data Access Control
sidebar_position: 50
---
Rill supports **granular access policies** that let you control:
- **Who can access your data**
- **What rows they can see**
- **Which dimensions and measures are visible**
Policies are based on user attributes such as **email address**, **domain**, or **custom attributes**. This avoids dashboard sprawl — instead of creating multiple dashboards for each audience, you can build _**one dashboard**_ and tailor it for many teams and use cases.
## How Does It Work?
Access policies are defined in the **metrics view** and/or **[dashboard YAML](/developers/build/dashboards/customization#define-dashboard-access)**.
There are three types of rules:
**General Access:** (`access`) A boolean expression deciding if a user can access the metrics view
```yaml
security:
access: "{{ .user.admin }} OR '{{ .user.domain }}' == 'example.com'"
```
:::info Dashboard access
`access` can be set on both the dashboard YAML and metrics view YAML and policies are combined using logical AND operations. If no policies are defined on the dashboard, they are derived from the metrics view. For most set-ups, setting the access on the metrics view is sufficient.
:::
**Row-level access** (`row_filter`) – a SQL expression that will be injected into the WHERE clause of all dashboard queries to restrict access to a subset of rows
```yaml
security:
row_filter: region = '{{ .user.region }}'
```
**Column-level access** (`include` or `exclude`) – lists of boolean expressions that determine which dimension and measure names will be available to the user
```yaml
security:
exclude:
- if: "'{{ .user.domain }}' != 'example.com'"
names:
- ssn
- id
```
When a user loads a dashboard, the policies are resolved in two phases:
1. The templating engine first replaces expressions like `{{ .user.domain }}` with actual values ([Templating reference](/developers/build/connectors/templating))
2. The resulting expression is then evaluated contextually:
- The `access` and `if` values are evaluated as SQL expressions and resolved to a `true` or `false` value
- The `row_filter` value is injected into the `WHERE` clause of the SQL queries used to render the dashboard
:::info What about MCP, and APIs?
Metrics views limit data access for all requests, including MCP integrations and custom APIs. When creating a token or copying from the AI tab, the user's attributes (such as email, domain, groups, and custom attributes) are automatically included in the request context. This ensures that the same security policies that apply to dashboard users also apply to programmatic access, maintaining consistent data governance across all access methods.
For more details, see [Service Tokens](/guide/administration/access-tokens/service-tokens).
:::
Typical use cases include:
- [**Granting or Restricting Access**](#restrict-data-access-to-users-matching-specific-criteria) to data and, as a result, dashboards
- [**Hiding specific dimensions and measures**](#conditionally-hide-a-dashboard-dimension-or-measure) from specific groups of users, creating a tailored dashboard experience
- [**Restricting Access to Internal users**](#hide-dimensions-or-measures-for-members-of-a-certain-group) of your organization, allowing specific dashboards to be viewed by internal users only
- [**Partner-filtered Dashboards**](#show-only-data-from-the-users-own-domain) where external users can only access the subset of their data
- [**Embedded**](#advanced-example-custom-attributes-embed-dashboards) use cases, passing custom attributes to Rill
:::tip Project Access Required
Access Policies assume that the user already has access to the project in Rill Cloud. For more information on user management, see our [User Management](/guide/administration/users-and-access/user-management) and [Project Management](/guide/administration/project-settings) documentation.
:::
## Creating Access Policies
There are two locations that control data access in Rill.
### Project Level Defaults
By default, when a user is granted access to your project, they have access to all metrics views and, if there is [no dashboard policy](/developers/build/dashboards/customization#define-dashboard-access), all dashboards. While this is the default behavior, it can be easily changed in the project's `rill.yaml`. This will lock down all metrics views and block all users who are not Rill Administrators or do not have 'example.com' as their domain.
:::tip Set project-wide security defaults
Configure default security policies for all metrics views and dashboards in your project.
[Learn more about security defaults →](/developers/build/project-configuration#metrics-views-security-policy)
:::
```yaml
metrics_views:
security:
access: "{{ .user.admin }} OR '{{ .user.domain }}' == 'example.com'"
row_filter: "partner_id IN (SELECT id FROM mapping WHERE partner_domain = '{{ .user.domain }}') OR '{{ .user.domain }}' = 'example.com'"
exclude:
- if: "'{{ .user.domain }}' != 'example.com'"
names:
- ssn
- id
```
### Object Specific Policies
You can define policies directly in a specific metrics view or dashboard YAML to override the project-level defaults.
```yaml
security:
access: '{{ has "partners" .user.groups }}'
row_filter: "domain = '{{ .user.domain }}'"
exclude:
- if: "'{{ .user.domain }}' != 'example.com'"
names:
- ssn
- id
```
:::tip Access Policy Behavior
When combining access policies from project defaults and object-specific policies, remember that the object level policies will overwrite the project level ones. Dashboard and metrics view policies are combined using logical AND operations.
:::
## Dashboard Access
Dashboards also have an `access` key that can add additional security to the metrics view. Both [explore](/developers/build/dashboards/customization#define-dashboard-access) and [canvas](/developers/build/dashboards/customization#define-dashboard-access) dashboards can set the following:
```yaml
security:
access: "'{{ .user.domain }}' == 'example.com'"
```
This will logically AND with your metrics view's access so ensure that a user who needs access to the dashboard passes **both** conditions.
:::tip complicated set-ups
Access Policies can get quite complicated as your use case grows and having to navigate multiple files to figure out why a user is able to or unable to access certain dashboards.
A few recommendations:
1. Only change project level access if absolutely necessary. (They get overwritten by object level security)
2. Dashboard access can be derived from the metrics view, only add extra policies on the dashboard if absolutely necessary as this gets combined with the metrics view using logical AND operations anyway.
3. Solve project access issues higher up in the [user](/guide/administration/users-and-access/user-management) / [usergroup](/guide/administration/users-and-access/usergroup-management) settings, and keep default project security rules.
:::
## User Attributes
- `.user.email` – the current user's email address, for example john.doe@example.com (string)
- `.user.domain` – the domain of the current user's email address, for example example.com (string)
- `.user.name` - the current user's name, for example John Doe (string)
- `.user.admin`– a boolean value indicating whether the current user is an org or project admin, for example true (bool)
- `.user.groups` - a list of user groups the user belongs to in the project's org (list of strings), e.g. ["marketing","sales","finance"]
- `.user.attribute` - where `attribute` is a custom variable that you can pass via an embedded dashboard from your application
Note: Rill requires users to confirm their email address before letting them interact with the platform, so a user cannot fake an email address or email domain.
## Testing Policies in Rill Developer
In development (on `localhost`), you can test your policies by adding "mock users" to your project and viewing the dashboard as one of them.
:::tip Test policies in Rill Developer
Use `mock_users` in rill.yaml to test your security policies before deploying.
[Learn more about testing security →](/developers/build/project-configuration#testing-security)
:::
In your project's `rill.yaml` file, add a `mock_users` section. Each mock user must have an `email` attribute and can optionally have `name` and `admin` attributes. For example:
```yaml
# rill.yaml
mock_users:
- email: john@yourcompany.com
name: John Doe
admin: true
- email: jane@partnercompany.com
groups:
- partners
- email: anon@unknown.com
```
On the dashboard page (provided you've added a policy), you'll see a "View as" button in the top right corner. Click this button and select one of your mock users. You'll see the dashboard as that user would see it.
### Rill Cloud
If you want to test what your users are seeing in Rill Cloud after deploying, you can find this in the dropdown of your account. You will see the actual users in the dropdown of this list, not the mock users defined in the rill.yaml file.

### Embedded Dashboards
When [requesting an embedded dashboard from Rill](/developers/embed/iframe) from your frontend, you can pass the `attributes` parameter with custom names to ensure that the resulting dashboard displays the correct information.
For more information, see [our embedding docs](/developers/embed/iframe#2-build-the-iframe-url-backend).
## Examples
### Restrict data access to users matching specific criteria
Let's say you want to restrict dashboard access to admin users or users whose email domain is `example.com`. Add the following clause to your metrics view's YAML:
```yaml
security:
access: "{{ .user.admin }} OR '{{ .user.domain }}' == 'example.com'"
```
:::note DEFAULT SECURITY IS FALSE
If the `security` section is defined and `access` is not, then `access` will default to `false`, meaning that it won't be accessible to anyone and users will need to be invited individually.
:::
### Restrict dashboard access to specific user groups
Group membership can be utilized to specify which users have access to a specific dashboard (using the templating function `has`). For example:
```yaml
security:
access: '{{ has "partners" .user.groups }}'
```
### Show only data from the user's own domain
You can limit the data available to the dashboard by applying a filter on the underlying data. Assuming the dashboard's underlying model has a `domain` column, adding the following clause to the dashboard's YAML will only show dimension and measure values for the current user's email domain:
```yaml
security:
access: true
row_filter: "domain = '{{ .user.domain }}'"
```
:::note FILTERS SHOULD BE VALID SQL
The `row_filter` value needs to be valid SQL syntax for a `WHERE` clause. It will be injected into every SQL query used to render the dashboard.
:::
### Conditionally hide a dashboard dimension or measure
You can include or exclude dimensions and measures based on a boolean expression. For example, to exclude dimensions named `ssn` and `id` for users whose email domain is not `example.com`:
```yaml
security:
access: true
exclude:
- if: "'{{ .user.domain }}' != 'example.com'"
names:
- ssn
- id
```
Alternatively, you can explicitly define the dimensions and measures to include using the `include` key. It uses the same syntax as `exclude` and automatically excludes all names not explicitly defined in the list. See the [Dashboard YAML](/reference/project-files/explore-dashboards) reference for details.
### Use wildcards to select all dimensions and measures
When defining inclusion policies, you can easily and automatically select all columns by using `names: '*'` as a wildcard. For example:
```yaml
security:
access: true
include:
- if: true
names:
- ssn
- id
- if: "{{ .user.admin }}"
names: '*'
```
Note that the `'*'` must be quoted (using single or double quotes) and **must** be provided as a scalar value, not as an entry in a list.
### Filter queries based on the user's groups
You can directly inject the groups that a user belongs to into the row filter itself, such as:
```yaml
security:
access: true
row_filter: "groups IN ('{{ .user.groups | join \"', '\" }}')"
```
### Hide dimensions or measures for members of a certain group
You can check group membership using the templating function `has`. For example:
```yaml
security:
access: true
exclude:
- if: '{{ has "partners" .user.groups }}'
names:
- cost
- profit
```
### Advanced Example: Mapping Dimensions and Attributes
For some use cases, the built-in user attributes do not provide sufficient context to correctly restrict access. For example, a dashboard for a multi-tenant SaaS application might have a `tenant_id` column, and external users should only be able to see data for the tenant they belong to.
To support this, ingest a separate data [source](/developers/build/connectors) containing mappings of user email addresses to tenant IDs and reference it in the row-level filter. This can be a locally created CSV file or any hosted data source.
For example, a locally created `mappings.csv` file in the `data` directory of your Rill project with the following contents:
```csv
email,tenant_id
john.doe@example.com,1
jane.doe@example.com,2
```
This needs to be ingested as a source in Rill like any other data source:
```yaml
# sources/mappings.yaml
type: local_file
path: data/mappings.csv
```
(In practice, you would probably ingest the data from a regularly updated export in S3 with a source refresh.)
We can now refer to the mappings data using a SQL sub-query as follows:
```yaml
security:
access: true
row_filter: "tenant_id IN (SELECT tenant_id FROM mappings WHERE email = '{{ .user.email }}')"
```
### Advanced Example: Custom attributes (Embed Dashboards)
Another use case for row access policies is to ensure that your embedded dashboard provides a specific view for your end users. During the [embed dashboard request](/developers/embed/iframe), you can pass custom attributes (other than the ones provided out-of-the-box) that map directly to a value within your Rill explore dashboard.
```yaml
security:
access: true
row_filter: >
dimension_1 = '{{ .user.custom_variable_1 }}' AND
dimension_2 = '{{ .user.custom_variable_2 }}'
```
In order to test the view of your embedded dashboard, you can add the same custom variables to [your mock users](#testing-policies-in-rill-developer) as seen below:
```yaml
- email: embed@rilldata.com
name: embed
custom_variable_1: Value_1
custom_variable_2: Value_2
```
### Advanced Example: Access to Dashboard in Rill and Embedded
While not common, there are use cases where a dashboard is used both in the Rill Cloud UI and as an embedded dashboard. In this case, passing a similar user attribute could suffice, but if you need to pass a custom attribute, you'll need to add an extra layer of logic to your dashboard.
```yaml
security:
access: "{{ .user.admin }} OR '{{ .user.domain }}' == 'example.com' {{- if .user.custom_variable_1 }} OR true {{- end }}"
```
---
###### Powering your Metrics View
---
title: Powering your Metrics View
sidebar_label: Underlying Model/Table
sidebar_position: 05
---
Once you have finished [building your model](/developers/build/models), you can create a metrics view to define measures and dimensions for your dashboard. The way you specify the underlying data source depends on your OLAP engine.
## Choosing Your Data Source
Rill supports [multiple OLAP engines](/developers/build/connectors/olap), and the engine you're using determines which YAML property you'll use in your metrics view:
- **Use `model`** for DuckDB and Rill-managed ClickHouse
- **Use `table`** for self-managed live connectors
## DuckDB and Rill-Managed ClickHouse
For DuckDB (the default engine) and Rill-managed ClickHouse, use the `model` property to reference your data model:
```yaml
# Metrics View YAML
# Reference documentation: https://docs.rilldata.com/reference/project-files/metrics-views
version: 1
type: metrics_view
model: example_model # Choose a model to underpin your metrics view
```
## Self-Managed Live Connectors
For self-managed live connectors (like your own ClickHouse, MotherDuck, or Druid instance), use the `table` property and specify connection details:
```yaml
# Metrics View YAML
# Reference documentation: https://docs.rilldata.com/reference/project-files/metrics-views
version: 1
type: metrics_view
database: default
connector: clickhouse
database_schema: billing
table: events # Choose a table to underpin your metrics view
```
For more information, refer to our [metrics view YAML configuration](/reference/project-files/metrics-views).
### Caching Query Results
By default, dashboard queries against metrics views backed by an external, self-managed connector (Snowflake, BigQuery, Databricks, your own ClickHouse, Druid, MotherDuck, Pinot, etc.) run live against the source on every interaction. For dashboards with many concurrent users or repeated drill-downs, this can drive up compute costs and add latency. Enable caching to reuse query results between users until the underlying data changes.
Caching is configured under the `cache` block on the metrics view. Because tables in live connectors are externally managed, caching is **off by default** — opt in by setting `cache.enabled: true`. By default, Rill invalidates the cache when the max timestamp of the metrics view's `timeseries` column changes. If you have a better indicator for when to invalidate the cache (for example, an ingest version column), use `cache.key_sql` to set a custom expression for the cache key.
```yaml
type: metrics_view
connector: snowflake
database: MY_DATABASE
database_schema: MY_SCHEMA
table: MY_TABLE
timeseries: created_at
dimensions:
- column: region
measures:
- name: total_revenue
expression: SUM(revenue)
cache:
enabled: true
key_sql: SELECT MAX(created_at) FROM MY_DATABASE.MY_SCHEMA.MY_TABLE
key_ttl: 5m
```
Rill runs `key_sql` against the metrics view's connector (re-evaluated at most once per `key_ttl`) and uses the returned scalar value as the cache key (together with a hash of the incoming metrics query). When the value changes — for example because a new row landed — the cache is invalidated and the next query repopulates it.
**Pros**
- **Lower source spend.** Repeat queries (multiple users on the same dashboard, back-and-forth filtering) are served from Rill's in-memory cache instead of hitting the source. On BigQuery this means fewer bytes scanned; on Snowflake and Databricks it means less warehouse compute; on ClickHouse, Druid, and Pinot it means fewer broker queries.
- **Faster cache hits.** Cache lookups skip warehouse warm-up, query queue time, and per-query slot allocation. End-to-end latency depends on your Rill deployment topology.
- **Tunable freshness.** `key_sql` ties cache invalidation to your data — typically a max event timestamp, an ingest version column, or a partition metadata lookup. `key_ttl` controls how often that signal is checked.
**Cons**
- **Up to `key_ttl` of staleness.** Between `key_sql` evaluations, new data in the source will not appear on the dashboard. Match `key_ttl` to your tolerable staleness; setting it lower means more frequent `key_sql` queries against the source.
- **`key_sql` itself runs on the source.** Make sure it's a cheap query: a `MAX()` on a clustered/partitioned column on Snowflake or Databricks, or a metadata lookup against `INFORMATION_SCHEMA.PARTITIONS` on BigQuery. A `MAX()` without a partition filter on BigQuery scans the whole table on every refresh.
- **Cache is split per security context.** Row-level security policies and user attributes are part of the cache key, so a dashboard that filters per user does not share cached results across users.
- **Memory usage.** Cached results live in Rill's in-memory cache; very high-cardinality dashboards with many distinct queries will evict older entries.
## Visual Metrics Editor
If you're using the UI to select your table, choosing a live connector will automatically configure the YAML with the correct `table`, `connector`, and `database_schema` fields.

---
###### Get Started with Metrics Views
---
title: Get Started with Metrics Views
description: Create metrics view using source data and models with time, dimensions, and measures
sidebar_label: What are Metrics Views?
sidebar_position: 00
---
A metrics view is a 'centralized framework' used to define and organize **key measures and dimensions** for your organization. Having a centralized layer allows an organization to easily manage and reuse calculations across various reports, dashboards, and data tools. Each metrics view is powered by a single [model or table](/developers/build/metrics-view/underlying-model).
Rill metrics views are analogous to the **semantic layer** and **metrics layer** concepts found in other business intelligence and analytics tools. It provides a consistent, reusable abstraction over your underlying data, enabling standardized definitions of business metrics that can be shared across different dashboards and reports.

In Rill, your metrics view is defined by _metric definitions_. Metric definitions are composed of:
* [_**model/table**_](/developers/build/metrics-view/underlying-model) - A data model or underlying table created with the concept of [One Big Table](/developers/build/models/models-101#one-big-table-and-dashboarding) that will power the metrics view.
* [_**timeseries**_](/developers/build/metrics-view/time-series) - A column from your model that will underlie x-axis data in Rill's Explore dashboards and Canvas dashboards. Time can be truncated into different time periods.
* [_**dimensions**_](/developers/build/metrics-view/dimensions) - Categorical columns from your data model whose values are shown in _leaderboards_ in explore dashboard and allow you to look at segments or attributes of your data (and filter/slice accordingly) as well as selectable axis in Canvas dashboard components.
* _[**measures**](/developers/build/metrics-view/measures)_ - Numerical aggregates of columns from your data model shown on the y-axis of the explore charts and canvas components and the "big number" summaries.
## Creating a Metrics view
Once your [model or underlying table](/developers/build/metrics-view/underlying-model) is ready to visualize, you'll need to create a metrics view to define your measures and dimensions. This can be done in a few ways. Either create a blank YAML file, use the Add metrics view button, or "Generate Metrics with AI" from the model.
### Create a Metrics view with Code
Copy the below into a blank YAML or use the Add -> metrics view to create a blank metrics view. Here you can start to define dimensions and measures as seen below.
```yaml
# Metrics View YAML
# Reference documentation: https://docs.rilldata.com/reference/project-files/metrics-views
version: 1
type: metrics_view
model: example_model # Choose a model to underpin your metrics view
timeseries: timestamp_column # Choose a timestamp column (if any) from your table
dimensions:
- column: category
display_name: "Category"
description: "Description of the dimension"
measures:
- expression: "SUM(revenue)"
display_name: "Total Revenue"
description: "Total revenue generated"
```
:::tip Set project-wide time defaults
Configure default time modifiers like first day of week and smallest time grain for all metrics views.
[Learn more about metrics view defaults →](/developers/build/project-configuration#metrics-views-time-modifiers)
:::
### Using the Visual Metrics Editor
When you add a metrics definition using the UI, a code definition will automatically be created as a YAML file in your Rill project within the metrics directory by default.

### Creating a Metrics View with AI
In order to streamline the process and get to a dashboard as quickly as possible, we've added the "Create Metrics with AI" and "Create Dashboard with AI" options! This will pass your schema to OpenAI to suggest measures and dimensions to get started with Rill.

:::tip Skipped creating a model?
You can now create dashboards directly for the connector panel. This will create a model, metrics view and dashboard all in one step.

:::
You can define your own OpenAI key by creating a [connector file](/reference/project-files/connectors#openai). If you want to disable metrics view generation with AI from your environment, please set the following in the `rill.yaml`:
```yaml
features:
ai: false
```
---
#### Models
---
###### Data Quality Tests
---
title: Data Quality Tests
sidebar_label: Data Quality Tests
sidebar_position: 14
---
## Overview
Data quality tests allow you to define automated checks that run whenever your models refresh. These tests ensure your data meets expectations and catch issues early in your pipeline.
Tests are defined in your model's YAML file using the `tests:` property. Each test runs a SQL query against your model's output. If the query returns any rows, the test fails and the error is recorded in the model's state.
## When to Use Data Quality Tests
:::tip Using live connectors? Use alerts instead
Data quality tests run when models refresh, which means they only work with models that Rill manages. If you're using [live connectors](/developers/build/connectors/olap) (ClickHouse, Druid, Pinot, StarRocks, etc.) where data lives in external systems, use [alerts](/developers/build/alerts) to monitor data quality on a schedule instead.
:::
Data quality tests are useful for:
- **Data Quality Checks** - Verify that your data meets business rules and constraints
- **Schema Validation** - Ensure expected columns exist and have correct data types
- **Referential Integrity** - Check relationships between tables
- **Range Validation** - Verify numeric values fall within expected ranges
- **Null Checks** - Ensure required fields don't contain null values
- **Uniqueness Constraints** - Verify primary keys and unique columns
- **Business Logic** - Validate complex business rules and calculations
## Defining Tests
Tests are defined in your model's YAML file under the `tests:` property. Each test requires:
- `name` - A descriptive name for the test
- Either `assert` or `sql` - The validation logic
### Basic Syntax
```yaml
type: model
sql: SELECT * FROM my_source
tests:
- name: Test Name
assert: column > 0 # OR
sql: SELECT * FROM model WHERE condition_is_bad
```
## Test Types
### Assert Tests
Assert tests use a WHERE clause condition that should be true for all rows. If any row fails the assertion (the condition is false), the test fails.
**Syntax:**
```yaml
- name: Test Name
assert:
```
The assertion is internally converted to:
```sql
SELECT * FROM model WHERE NOT ()
```
**Examples:**
```yaml
tests:
# Check for null values
- name: No Null Campaign ID
assert: campaign_id IS NOT NULL
# Range validation
- name: Valid Bid Price
assert: bid_price >= 0 AND bid_price <= 100
# Value constraints
- name: Valid Ad Status
assert: status IN ('active', 'paused', 'completed', 'draft')
# Multiple conditions
- name: Valid Impression Count
assert: impressions >= 0 AND impressions <= 1000000000
```
### SQL Tests
SQL tests use a complete SQL query that should return zero rows. If any rows are returned, the test fails.
**Syntax:**
```yaml
- name: Test Name
sql: SELECT * FROM model WHERE
```
**Examples:**
```yaml
tests:
# Row count validation
- name: Minimum Impression Count
sql: SELECT 'Too few impressions' WHERE (SELECT COUNT(*) FROM model) < 1000
# Aggregate validation
- name: Positive Total Spend
sql: SELECT 'Negative spend detected' WHERE (SELECT SUM(spend) FROM model) < 0
# Duplicate detection
- name: No Duplicate Impression IDs
sql: |
SELECT impression_id, COUNT(*) as count
FROM model
GROUP BY impression_id
HAVING COUNT(*) > 1
# Referential integrity
- name: Valid Campaign References
sql: |
SELECT i.campaign_id
FROM model i
LEFT JOIN campaigns c ON i.campaign_id = c.id
WHERE c.id IS NULL
# Data completeness
- name: All Expected Ad Formats Present
sql: |
SELECT missing_format FROM (
VALUES ('banner'), ('video'), ('native')
) AS expected(missing_format)
WHERE missing_format NOT IN (SELECT DISTINCT ad_format FROM model)
```
## Complete Example
Here's a comprehensive example showing various validation patterns:
```yaml
# models/ad_impressions.yaml
type: model
sql: SELECT * FROM raw_impressions
tests:
# Basic null checks
- name: Impression ID Not Null
assert: impression_id IS NOT NULL
- name: Campaign ID Not Null
assert: campaign_id IS NOT NULL
# Range validations
- name: Valid Bid Price
assert: bid_price > 0 AND bid_price < 100
- name: Valid Impression Date
assert: impression_date >= '2020-01-01' AND impression_date <= CURRENT_DATE
# Status validation
- name: Valid Campaign Status
assert: status IN ('active', 'paused', 'completed', 'draft')
# Uniqueness check
- name: No Duplicate Impression IDs
sql: |
SELECT impression_id, COUNT(*) as count
FROM ad_impressions
GROUP BY impression_id
HAVING COUNT(*) > 1
# Row count validation
- name: Minimum Impressions Present
sql: SELECT 'Too few impressions' WHERE (SELECT COUNT(*) FROM ad_impressions) < 1000
# Aggregate validation
- name: Positive Total Spend
sql: SELECT 'Negative spend' WHERE (SELECT SUM(spend) FROM ad_impressions) < 0
# Complex business logic
- name: Clicks Must Have Impressions
sql: SELECT * FROM ad_impressions WHERE clicks > 0 AND impressions = 0
- name: Click Timestamp After Impression
sql: SELECT * FROM ad_impressions WHERE click_timestamp < impression_timestamp
# Referential integrity
- name: Valid Campaign References
sql: |
SELECT i.campaign_id
FROM ad_impressions i
LEFT JOIN campaigns c ON i.campaign_id = c.campaign_id
WHERE c.campaign_id IS NULL
```
## Test Execution
Tests are executed automatically when your model is refreshed:
1. **Model Refresh** - The model's SQL query runs and produces output
2. **Test Execution** - Each test query runs against the model's output table
3. **Result Recording** - Test failures are recorded in the model state
4. **Model Status** - The model remains available even if tests fail
### Test Behavior
- Tests run **after** successful model refresh
- A failing test does **not** prevent the model from being available for queries
- Test results are stored in the model's state and visible in the [Rill logs](/reference/cli/project/logs)
- All tests run independently - one failure doesn't stop other tests
- Tests can reference the model's output using the model name
## Viewing Test Results
Test results are stored in the model state and visible in:
- **Model State** - Access via the runtime API in the `test_errors` field
- **Logs** - Test failures are logged during model reconciliation
When a test fails, the error message includes:
- Test name
- Number of rows that failed the test
- Sample of the failing rows (if applicable)
## Best Practices
### Test Naming
Use descriptive names that clearly indicate what's being validated:
```yaml
# Good names
- name: No Null Campaign IDs
- name: Spend Within Valid Range
- name: All Impressions Have Valid Status
# Less clear names
- name: Test 1
- name: Check Data
- name: Validation
```
### Test Organization
Group related tests together and add comments:
```yaml
tests:
# Null checks
- name: Impression ID Not Null
assert: impression_id IS NOT NULL
- name: Campaign ID Not Null
assert: campaign_id IS NOT NULL
# Range validations
- name: Valid Bid Price
assert: bid_price > 0
- name: Valid Date Range
assert: impression_date >= '2020-01-01'
```
### Understanding Assert vs SQL Syntax
**Assert Syntax** - Define conditions that should be true for all rows:
- You write: `assert: value > 0`
- Rill converts this to: `SELECT * FROM model WHERE NOT (value > 0)`
- Tests **pass** if no rows are returned (all rows satisfy the condition)
- Tests **fail** if any rows are returned (violations found)
**SQL Syntax** - Write custom queries that return failing rows:
- You write: `sql: SELECT * FROM model WHERE value <= 0`
- Your query should explicitly return rows that represent failures
- Tests **pass** if the query returns an empty result set
- Tests **fail** if the query returns any rows
:::tip Key Difference
With `assert`, you define what should be **true**. With `sql`, you query for what is **wrong**.
:::
### Choosing Between Assert and SQL
**Use Assert when:**
- Testing row-level conditions
- Checking simple constraints
- The logic is straightforward
- You want Rill to handle the "NOT" logic for you
**Use SQL when:**
- Testing aggregate values (COUNT, SUM, AVG)
- Checking relationships between tables
- Implementing complex validation logic
- You need more control over the error message
- You prefer to explicitly write the failure query
### Performance Considerations
- Tests add time to your model refresh cycle
- Complex tests with joins or aggregations can be expensive
- Consider the trade-off between test coverage and refresh time
- Use indexes on columns referenced in test queries when possible
### Testing Strategy
**Start with critical validations:**
1. Null checks on required fields
2. Uniqueness constraints
3. Referential integrity
4. Range validations
**Add domain-specific tests:**
1. Business rules
2. Data quality checks
3. Completeness validations
**Monitor and iterate:**
1. Review test failures regularly
2. Add tests when data issues are discovered
3. Remove or update tests that are no longer relevant
## Working with Incremental Models
Tests work with incremental models and run after each incremental refresh:
```yaml
type: model
incremental: true
sql: SELECT * FROM raw_impressions WHERE impression_timestamp > (SELECT MAX(impression_timestamp) FROM {{ .self }})
tests:
- name: No Null Impression Timestamps
assert: impression_timestamp IS NOT NULL
- name: No Future Impression Timestamps
assert: impression_timestamp <= CURRENT_TIMESTAMP
```
The tests validate the **entire model output**, not just the newly added rows.
## Working with Partitioned Models
Tests also work with partitioned models:
```yaml
type: model
incremental: true
partitions:
sql: SELECT DISTINCT impression_date FROM raw_impressions
sql: SELECT * FROM raw_impressions WHERE impression_date = '{{ .partition.impression_date }}'
tests:
- name: Valid Impression Dates
assert: impression_date IS NOT NULL
- name: Partition Date Matches
assert: impression_date = '{{ .partition.impression_date }}'
```
Tests run against the complete model output after all partitions are processed.
## Examples
### Checking for Duplicates
```yaml
- name: No Duplicate Impression IDs
sql: |
SELECT impression_id, COUNT(*) as count
FROM model
GROUP BY impression_id
HAVING COUNT(*) > 1
```
### Validating Relationships
```yaml
- name: Valid Campaign References
sql: |
SELECT i.campaign_id
FROM model i
LEFT JOIN campaigns c ON i.campaign_id = c.id
WHERE c.id IS NULL
```
### Checking Completeness
```yaml
- name: All Expected Ad Formats Present
sql: |
SELECT expected_format
FROM (VALUES ('banner'), ('video'), ('native')) AS expected(expected_format)
WHERE expected_format NOT IN (SELECT DISTINCT ad_format FROM model)
```
### Aggregate Validations
```yaml
- name: CTR Within Expected Range
sql: |
SELECT 'CTR out of range' as error
WHERE (SELECT SUM(clicks) * 1.0 / NULLIF(SUM(impressions), 0) FROM model) > 0.5
```
### Date Range Checks
```yaml
- name: Valid Impression Date Range
assert: impression_date >= '2020-01-01' AND impression_date <= CURRENT_DATE
- name: Impressions Within Last Year
assert: impression_date >= CURRENT_DATE - INTERVAL '1 year'
```
:::warning Limitations
- Tests only run when the model is refreshed
- Failing tests do not prevent the model from being queryable
- Tests cannot modify data - they are read-only validations
- Test queries should complete reasonably quickly to avoid long refresh times
:::
---
###### Schedule Your Data Refresh
---
title: Schedule Your Data Refresh
description: Manage refresh schedules for models deployed to Rill Cloud
sidebar_label: Scheduled Refreshes
sidebar_position: 15
---
When creating or updating a model in Rill Cloud, you also have the option to configure how often the underlying model is refreshed (and thus ingested into the underlying OLAP layer powering Rill models and dashboards). By default, models are refreshed manually, but this can also be automated to a predefined schedule. This is handled through the underlying [model settings](/reference/project-files/models) and/or [project YAML](/reference/project-files/rill-yaml#project-wide-defaults) using standard cron or Go duration syntax.
:::tip Configuring model refreshes for Cloud deployments
It is generally strongly recommended to configure model refreshes when [deploying a project](/developers/deploy/deploy-dashboard) to Rill Cloud to ensure that your production data (and dashboards) _remains up-to-date_. The interval that you should set really depends on how often your own data is being refreshed. Furthermore, while it is technically possible to configure model refreshes for Rill Developer as well, Rill Developer is primarily used for local development and thus typically does not require working with the most up-to-date data (local model refreshes that occur too often could also lead to resource constraints on your local machine). For more details, please see our pages on [environments](/developers/build/connectors/credentials#variables), [templating](/developers/build/connectors/templating), and [performance optimization](/developers/build/models/performance).
:::
## Configuring Model Refresh Individually
To specify a model refresh schedule for a particular model, this can be handled using the `refresh` property in the underlying YAML file. For example, to set a daily refresh for a model, you can do the following:
```yaml
refresh:
every: 24h
```
Similarly, if you would like to utilize cron syntax, the following example would update a model every 15 minutes:
```yaml
refresh:
cron: '*/15 * * * *'
```
:::note Model settings
For more details about available model configurations and properties, check our [model YAML](/reference/project-files/models) reference page.
:::
## Configuring a Project-Wide Default
You can also specify a project-wide refresh schedule that will apply to all models by default. This can be done through the `rill.yaml` file. More details can be found [here](/reference/project-files/rill-yaml#project-wide-defaults).
:::tip Set project-wide defaults
You can set a default refresh schedule for all models in your project.
[Learn more about project defaults →](/developers/build/project-configuration#model-refresh-schedule)
:::
Using the same example as above, the following sets a project-wide default of refreshing models every 24 hours:
```yaml
models:
refresh:
every: 24h
```
Similarly, the following would use cron syntax to set a project-wide configuration of refreshing models by default every 15 minutes (unless overridden at the individual model level):
```yaml
models:
refresh:
cron: '*/15 * * * *'
```
:::info Did you know?
If you have both a project-wide default and model-specific refresh schedule _configured in the same project_, the model-specific refresh will **override** the project default based on how inheritance works in Rill. Otherwise, if not specified, the project-wide default will be used instead!
:::
## Running Scheduled Source Refreshes in Development
As an exception, scheduled source refreshes specified using `refresh:` are not applied in the `dev` environment by default. If you want to run or test scheduled refreshes in local development, you can override this behavior using the `run_in_dev` property:
```yaml
refresh:
cron: 0 * * * *
run_in_dev: true
```
---
###### Incremental Models
---
title: Incremental Models
description: Create Incremental Models
sidebar_label: Incremental Models
sidebar_position: 20
---
Incremental models help with the ingestion of large datasets by allowing a dataset to be broken down into smaller sections for ingestion, rather than reading the entire dataset at once. Unlike [standard SQL models](/developers/build/models/sql-models) that are created via a .sql file, incremental models are defined in a YAML file and are used when a large dataset needs to be incrementally ingested to improve ingestion costs and time.
:::note Take a look at the Reference!
If you are unsure about the required parameters, please review the [reference page for Advanced Models](/reference/project-files/models).
:::
Rill supports incremental models on either cloud storage or data warehouses, but the parameters to set these up will be different. Cloud storage requires the `glob` parameter while data warehouses will need to use `sql`.
See [our reference documentation](/reference/project-files/models) for more information.
:::tip Need help setting up Incremental Models?
Please [reach out to us](/contact) if you have any questions regarding incremental modeling!
:::
## Creating an Incremental Model
In order to enable an incremental model, you will need to set the following: `incremental: true`.
```yaml
type: model
incremental: true
sql: # some SQL query from source_table
```
:::warning Duplicate Data
Incremental models default to an append strategy, and with neither `state` nor `partition` defined, your data will append data per incremental refresh from the source table. This will result in duplicate data and is not recommended. Instead, use the `merge_strategy` with a `unique_key` to ensure duplicate data is not ingested.
:::
:::warning Late Arriving Data
If you have late arriving data, you will need to keep this in mind when designing your incremental model. If you simply use max(date) from the source, you may risk leaving out late arriving data. Depending on your specific use case, you might consider a larger time difference and use a `merge` as your `incremental_strategy`.
:::
### Incremental Models with State Defined (Optional)
If your data is not [partitioned](/developers/build/models/partitioned-models), you can define the incremental model with a predefined `state` parameter. This is only useful for multi-connector incremental ingestion such as BigQuery to DuckDB.
```yaml
type: model
incremental: true
connector: bigquery
state:
sql: SELECT MAX(date) as max_date
sql: |
SELECT ... FROM events
{{ if incremental }}
WHERE event_time > '{{.state.max_date}}'
{{end}}
output:
connector: duckdb
```
Once state is defined in an incremental model, its value can be used as a variable in your SQL statement. In the above example, the state gets the most recent date from the model and when incrementally refreshing, ingests data for events that are more recent than the state's max_date.
:::tip
You can verify the current value of your state in the left-hand panel under Incremental Processing.
:::
### Refreshing an Incremental Model
When you are testing with incremental models in Rill Developer, you will notice a change in the refresh functionality. Instead of a full refresh, you are given the option for `incremental refresh`.

:::tip What's the difference?
Once increments are enabled on a model, this grants you the ability to refresh the model in increments, instead of loading the full data each time. This is handy when your data is massive and re-ingesting the data may take time. For a project in production, this allows for less downtime when needing to update your dashboards when the source data is updated.
There are times when a full refresh may be required. In these cases, running the full refresh is equivalent to running a normal refresh with incremental disabled.
:::
When selecting to refresh incrementally, what is being run in the CLI is:
```bash
rill project refresh --local --model
```
Keep in mind that if you select `Full refresh`, this will start the ingestion of **all of your data** from scratch. Only use this when absolutely required. When running a full refresh, the CLI command is:
```bash
rill project refresh --local --model --full
```
## Model Change Modes
Configure how changes to your model specifications are applied:
```yaml
# model.yaml
change_mode: reset # Options: reset (default), manual, patch
```
- `reset`: changing the model automatically leads to a full refresh (default)
- `manual`: changing the model stops refreshes until a manual incremental or full refresh is run
- `patch`: changing the model automatically changes to the new logic without a reset (only works for models with `incremental: true`)
---
###### Incremental Partitioned Models
---
title: Incremental Partitioned Models
description: Create incremental partitioned models
sidebar_label: Incremental + Partitioned Models
sidebar_position: 30
---
Putting the two concepts together, it is possible to create an incremental partitioned model. Doing so will allow you to not only partition the model but also refresh only the partition that you need and incrementally ingest partitions.
:::note Need help?
If you need any assistance with setting up an incremental partitioned model, [reach out](/contact) to us for assistance!
:::
:::tip Looking for an example?
If you're looking for a working example, take a look at [my-rill-tutorial in our examples' repository](https://github.com/rilldata/rill-examples).
:::
As we already know how to set these up separately, let's see what changes in the UI when we enable both on a single model. In the following example, note that incremental modeling is enabled and partitions are defined by the Google Cloud Storage directory.
```yaml
type: model
incremental: true
refresh:
cron: "0 8 * * *"
partitions:
glob:
path: gs://rilldata-public/github-analytics/Clickhouse/2024/*/*
partition: directory
sql: |
SELECT *
FROM read_parquet('{{ .partition.uri }}/commits_*.parquet')
WHERE '{{ .partition.uri }}' IS NOT NULL
```
### Refreshing Partitions in Incremental Models
When this model loads, you will be able to both view the partitions and select a specific partition to refresh via the UI in Rill Developer. Unlike **partitioned-only** models, a new button is added in each of the partitions.

Likewise, if you refresh using the **CLI**:
```bash
rill project refresh --model CH_incremental_commits_directory --local --partition ba9f71625de8e042cabf3333576d502c
Refresh initiated. Check the project logs for status updates.
```
## How Incremental Partitioned Models Work
### Initial Ingestion:
When a model is first created, an initial ingestion will occur to bring in all the data, also known as a `Full Refresh`. All refreshes after this will be considered an `incremental refresh`. Note in the below image, the source table writes each section of data to a specific partition as mapped in the YAML file.

### Additional Partition:
If you add an additional partition to the source table, on the next manual or automatic refresh, Rill will detect the new partition and **only** add the additional partition to the model, as you can see in the diagram, the **blue** additional partition is added in its own partition in the partitioned model. If the other partitions have not been modified, these will not be touched.

### Modify Existing Partition:
If you modify any of the already existing partitions, **yellow**, Rill will re-ingest just the modified file during the scheduled refresh by checking the `last_modified_date` parameter.

---
###### Models in Rill
---
title: Models in Rill
sidebar_label: Build
sidebar_position: 0
---
Rill provides users the option to build various types of models based on different use cases. Take a look through the list of available types of models!
### Fundamentals
- [**Getting Started with Models**](/developers/build/models/models-101) - What are models and important topics to remember as you create your own.
- [**Differences between SQL and YAML Models**](/developers/build/models/model-differences) - Compare SQL and YAML models and learn when to use each.
- [**SQL Models**](/developers/build/models/sql-models) - Transform and prepare your data with a basic SQL file
### Data Ingestion
- [**Source Models**](/developers/build/models/source-models) - Ingest data from various sources
- [**Data Quality Tests**](/developers/build/models/data-quality-tests) - Add data quality tests to validate your models
- [**Scheduled Refreshes**](/developers/build/models/data-refresh) - Schedule your data refresh
### Advanced YAML Features
- [**Incremental Models**](/developers/build/models/incremental-models) - Increment your model to avoid large ingestion times
- [**Partitioned Models**](/developers/build/models/partitioned-models) - Split your large data into smaller sizes
- [**Incremental + Partitioned Models**](/developers/build/models/incremental-partitioned-models) - Combine the concepts of Incremental Models and Partitioned Models into a single file
- [**Staging Models**](/developers/build/models/staging-models) - When data is not directly available to ingest
### Environment Management
- [**Environment Templating**](/developers/build/models/templating) - Separate your production and development settings
### Performance Optimization
- [**Optimize your Models**](/developers/build/models/performance) - Optimize model performance and data refresh strategies
---
###### When to use SQL vs YAML
---
title: When to use SQL vs YAML
description: Create models from source data and apply SQL transformations
sidebar_label: When to use SQL vs YAML
sidebar_position: 03
---
In Rill, there are two types of data models:
- [SQL models](/developers/build/models/model-differences#sql-models)
- [YAML models](/developers/build/models/model-differences#yaml-models)
For most use cases, SQL models, _the default_, are sufficient to transform your data to prepare for visualization. SQL models are built using SQL `SELECT` statements applied to your source data. Under the hood, SQL models are created as views in DuckDB and can be [materialized](/developers/build/models/performance#materialization) as tables when needed.
For more complex modeling and [data ingestion](/developers/build/models/source-models), YAML models are used. By using a YAML approach, we are able to fine-tune the model's settings to enable partitions, incremental modeling, refreshes, and more.
:::tip Avoid Pre-aggregated Metrics
Rill works best for slicing and dicing data, meaning keeping data closer to raw to retain that granularity for flexible analysis. When loading data, be careful with adding pre-aggregated metrics like averages, as that could lead to unintended results like a sum of an average. Instead, load the two raw metrics and calculate the derived metric in your model or dashboard.
:::
## SQL Models
### When to use SQL Models?
For most users working with DuckDB-backed Rill projects, SQL models provide everything needed to transform and prepare data for visualizations. These models are the default option when using the UI and offer full functionality for data transformation.
### Creating a SQL Model
When using the UI to create a new model, you'll see something similar to the screenshot below. You can also create a model directly from the connector UI in the bottom left by selecting the "...". This will create a `select * from underlying_table` as a SQL model file.

## YAML Models
Unlike SQL models, YAML file models allow you to fine-tune a model to perform additional capabilities such as pre-exec, post-exec SQL, partitioning, and incremental modeling. This is an important addition to modeling, as it allows users to customize the model's build process. In the case of partitions and incremental modeling, this will reduce the amount of data ingested into Rill at each interval and provide insight into specific issues per partition. Another use case is when using [multiple OLAP engines](/developers/build/connectors/olap/multiple-olap), which allows you to define where a SQL query is run.
### When to use YAML Models
For the majority of users on a DuckDB-backed Rill project, YAML models are not required. When a project grows larger and refreshing entire datasets becomes a time-consuming and costly task, we introduce incremental ingestion to help alleviate the problem. Along with incremental modeling, we use partitions to divide a dataset into smaller, more manageable partitions. After enabling partitions, you will be able to refresh individual partitions of data when required.
Another use case is when using multiple OLAP engines. This allows you to specify where your SQL query is running. When both DuckDB and ClickHouse are enabled in a single environment, you will need to define `connector: duckdb/clickhouse` in the YAML to tell Rill where to run the SQL query, as well as define the `output` location. For more information, refer to the [YAML reference](/reference/project-files/models).
### Types of YAML Models
1. [Source Models](/developers/build/models/source-models)
2. [Incremental Models](/developers/build/models/incremental-models)
3. [Partitioned Models](/developers/build/models/partitioned-models)
4. [Incremental + Partitioned Models](/developers/build/models/incremental-partitioned-models)
5. [Staging Models](/developers/build/models/staging-models)
### Creating a YAML Model
You can get started with an advanced model using the following code block:
```yaml
# Model YAML
# Reference documentation: https://docs.rilldata.com/reference/project-files/models
type: model
connector: duckdb
sql: select * from
output:
connector: duckdb
table: output_name
```
Please refer to [our reference documentation](/reference/project-files/models) linked above for the available parameters to set in your model.
---
###### Introduction to Models
---
title: Introduction to Models
description: Create models from source data and apply SQL transformations
sidebar_label: Models 101
sidebar_position: 00
---
Models in Rill enable data transformation, preparation, and enrichment through SQL queries and YAML configurations. They serve as the foundation for creating clean, structured datasets that power your metrics views and dashboards.
Data models are built using SQL SELECT statements applied to your source data. They allow you to join, transform, and clean data.
### SQL Transformations
By default, data transformations in Rill Developer are powered by DuckDB and its dialect of SQL (DuckDB SQL). Please visit the [DuckDB SQL documentation](https://duckdb.org/docs/sql/introduction) to learn how to write your queries.
You can change the default [OLAP engine](https://docs.rilldata.com/developers/build/connectors/olap) for [the entire project](https://docs.rilldata.com/reference/project-files/rill-yaml#configuring-the-default-olap-engine) or [a specific metrics view](https://docs.rilldata.com/reference/project-files/metrics-views). You will need to define the connector credentials within your Rill project or via environment variables.
:::tip Supported OLAP engines for modeling
We support modeling on [ClickHouse\*](/developers/build/connectors/olap/clickhouse), [DuckDB](/developers/build/connectors/olap/duckdb), and [MotherDuck\*](/developers/build/connectors/olap/motherduck). For more information, see each OLAP engine page for further details.
\* indicates some caveats with modeling, and we encourage you to read the documentation before getting started.
:::
For additional tips on advanced expressions (either in models or measure definitions), visit our [advanced expressions page](/developers/build/metrics-view).
### Intermediate Processing
Models can also be cross-referenced with each other to produce the final output for your dashboard. This approach enables more complex, intermediate data transformations to achieve your final data source. Common modeling patterns include:
- Lookups for id/name joins
- Unnesting and merging complex data types
- Combining multiple sources with data cleansing or transformation requirements
## Data Preview and Validation
### Table Preview
Rill automatically generates a preview of your data (first 150 rows) to help verify that the output table structure is correct and identify any potential issues that need to be addressed in the SQL configuration, such as data type detection problems.
### Schema Details
The right panel displays comprehensive information about your dataset and column contents:
- **Dataset Overview**: Total row and column counts
- **Data Quality Metrics**: Number of dropped rows and columns
- **Column Analysis**:
- Column names and data types
- Distinct value counts for string columns
- Basic numeric statistics (minimum, maximum, median, etc.)
This information helps you validate your model configuration and ensure data quality before proceeding with the full data ingestion.

## One Big Table and Dashboarding
The power of this approach lies in translating many ad hoc questions into a data framework that can answer a class of questions at scale. For example, high-level company insights (how much revenue did we make last week?) become more actionable for employees when contextualized to their role (how did my campaign increase revenue last week?).
To experience the full potential of Rill, model your data sources into "One Big Table" – a granular resource that contains as much information as possible and can be rolled up in a meaningful way. This flexible OBT can be combined with a generalizable [metrics definition](/developers/build/metrics-view) to enable ad hoc slice-and-dice discovery through Rill's interactive dashboard.
:::tip Materializing metrics-powered models
We recommend materializing the model that powers your [metrics view](/developers/build/metrics-view). You can materialize a SQL model by adding this to the top of the file. This will greatly improve the performance of your dashboards.
```sql
-- @materialize: true
```
:::
---
###### Partitioned Models
---
title: Partitioned Models
description: Create Partitioned Models
sidebar_label: Partitioned Models
sidebar_position: 25
---
## What are Partitions?
In Rill, partitions are a special type of state that allows you to explicitly partition the model into parts. Depending on whether your data is in cloud storage or a data warehouse, you can use the `glob` or `sql` parameters. This is useful when a specific partition is failing to ingest; you can specify to reload only that specific partition.
### Defining a Partition in a Model
Under the `partitions:` parameter, you will define the pattern in which your data is stored. Both SQL and glob patterns support [templating](/developers/build/connectors/templating) and can be used to separate `dev` and `prod` instances.
### SQL
When defining your SQL partitions, it is important to understand the data that you are querying and creating a partition that makes sense. For example, you might select a distinct customer_name per partition, or partition the SQL by a chronological partition, such as month.
#### Using DuckDB for Partition Queries
By default, partition queries use DuckDB (Rill's embedded OLAP engine):
```yaml
partitions:
sql: SELECT range AS num FROM range(0,100) #num is the partition variable and can be referenced as {{partition.num}}
#sql: SELECT DISTINCT customer_name as cust_name from table #results in {{partition.cust_name}}
dev:
partitions:
sql: SELECT range AS num FROM range(0,10)
sql: SELECT * from table where column = {{partition.num}}
```
#### Using Other Connectors for Partition Queries
You can query partitions directly from data sources like **Athena**, **BigQuery**, **MySQL**, **Postgres**, **Redshift**, or **Snowflake** by specifying a `connector` in the `partitions` section. This is particularly useful when:
- You want to leverage native partitioning features (like BigQuery's `_PARTITIONTIME`)
- You need to query large tables that benefit from the warehouse's optimization
**Data Warehouses**
Athena:
```yaml
type: model
partitions:
connector: athena
sql: |
SELECT DISTINCT year, month
FROM s3_data_partitioned
WHERE year >= 2024
connector: athena
sql: |
SELECT * FROM s3_data_partitioned
WHERE year = {{ .partition.year }}
AND month = {{ .partition.month }}
output:
connector: duckdb
```
BigQuery:
```yaml
type: model
partitions:
connector: bigquery
sql: |
SELECT DISTINCT _PARTITIONTIME AS partition_time
FROM `project.dataset.table`
WHERE TIMESTAMP_TRUNC(_PARTITIONTIME, MONTH) = TIMESTAMP("2025-08-01")
connector: bigquery
sql: |
SELECT * FROM `project.dataset.table`
WHERE _PARTITIONTIME = '{{ .partition.partition_time }}'
output:
connector: duckdb
```
Redshift:
```yaml
type: model
partitions:
connector: redshift
sql: |
SELECT DISTINCT DATE_TRUNC('month', transaction_date) AS month
FROM transactions
WHERE transaction_date >= '2024-01-01'
connector: redshift
sql: |
SELECT * FROM transactions
WHERE DATE_TRUNC('month', transaction_date) = '{{ .partition.month }}'
output:
connector: duckdb
```
Snowflake:
```yaml
type: model
connector: snowflake
partitions:
connector: snowflake
sql: |
select
DISTINCT date_trunc('YEAR', release_date) as "year"
from
rillqa.public.horror_movies
where "year" > '1999-01-01' limit 3
sql: select * from rillqa.public.horror_movies where date_trunc('YEAR', release_date) = '{{ .partition.year }}'
output:
connector: duckdb
```
**OLTP Databases**
MySQL:
```yaml
type: model
partitions:
connector: mysql
sql: |
SELECT DISTINCT DATE(order_date) AS order_day
FROM orders
WHERE order_date >= '2025-01-01'
connector: mysql
sql: |
SELECT * FROM orders
WHERE DATE(order_date) = '{{ .partition.order_day }}'
output:
connector: duckdb
```
Postgres:
```yaml
type: model
partitions:
connector: postgres
sql: |
SELECT DISTINCT DATE_TRUNC('day', created_at) AS partition_day
FROM events
WHERE created_at >= '2025-01-01'
connector: postgres
sql: |
SELECT * FROM events
WHERE DATE_TRUNC('day', created_at) = '{{ .partition.partition_day }}'
output:
connector: duckdb
```
:::tip Why use multiple connectors?
Using Athena, BigQuery, MySQL, Postgres, Redshift, or Snowflake for partition discovery and data extraction, then outputting to DuckDB, gives you:
- **Best of both worlds**: Leverage your warehouse's partitioning and scale for extraction
- **Fast dashboards**: DuckDB provides extremely fast query performance for end-user dashboards
- **Cost optimization**: Only query what you need from your warehouse, reducing scan costs
:::
:::tip Using the SQL partition in the YAML
Depending on the column name of the partition, you can reference the partition using `{{ .partition. }}` in the model's SQL query.
```yaml
partitions:
sql: SELECT range AS num FROM range(0,10)
sql: SELECT {{ .partition.num }} AS num, now() AS inserted_on {{if dev}} limit 1000 {{end}}
```
:::
### glob
When defining the glob pattern, you will need to consider whether you'd partition the data by folder or file.
In the first example, we are partitioning by each file with the suffix data.csv.
```yaml
partitions:
glob: gs://my-bucket/y=2025/m=03/d=15/*data.csv
#glob: gs://my-bucket/{{if dev}}y=2025/m=03/d=15{{else}}**{{end}}/*data.csv
```
Or, you can define each glob separately.
```yaml
partitions:
glob:
path: 'gs://my-bucket/**/*.parquet'
dev:
partitions:
glob:
path: 'gs://my-bucket/y=2025/m=03/d=15/*.parquet'
```
If you'd prefer to partition it by folder, you can add the partition parameter and define it as `directory`.
```yaml
glob:
path: gs://rendo-test/**/*data.csv
partition: directory #hive
```
:::tip Using the glob partition in the YAML
The glob partition has a predefined `{{ .partition.uri }}` reference to use in the model's SQL query.
```yaml
partitions:
glob:
connector: gcs
path: gs://path/to/file/**/*.parquet
sql: SELECT * FROM read_parquet('{{ .partition.uri }}')
```
:::
### Windowed Glob Partitions
Use `start` and `end` to filter which partitions are processed based on their path — useful for backfills, time-bounded ingestion, or limiting how much data is reprocessed.
- **`start`** — lower bound (inclusive): only partitions with paths ≥ this value are included.
- **`end`** — upper bound (exclusive): only partitions with paths < this value are included.
- **`last`** — limit to the last N partitions (by lexicographic path order). This hard limit always applies, including on the first run when no partitions have been ingested yet. Once partitions have been ingested, it also raises the lower bound to the Nth-from-last successfully ingested partition, forming a rolling window that re-lists recent partitions.
```yaml
# Fixed window: only process a specific month
partitions:
glob:
path: s3://bucket/year=*/month=*/day=*
partition: directory
start: s3://bucket/year=2026/month=03
end: s3://bucket/year=2026/month=04
sql: SELECT * FROM read_parquet('{{ .partition.uri }}/*.parquet')
```
```yaml
# Rolling window: reprocess last 30 days
partitions:
glob:
path: s3://bucket/year=*/month=*/day=*
partition: directory
last: 30
sql: SELECT * FROM read_parquet('{{ .partition.uri }}/*.parquet')
```
:::note
`last` is not compatible with `transform_sql`.
:::
### Viewing Partitions in Rill Developer
Once `partitions:` is defined in your model, a new button will appear in the right-hand panel: `View Partitions`. When selecting this, a new UI will appear with all of your partitions and more information on each. Note that these can be sorted by all, pending, and errors.

You can sort the view by `all partitions`, `pending partitions`, and `error partitions`.
- **all partitions**: shows all the available partitions in the model.
- **pending partitions**: shows the partitions that are waiting to be processed.
- **error partitions**: displays any partitions that errored during ingestion.
### Viewing Partitions in the CLI
Likewise to the UI, you can view the partitions of a model within the CLI.
```
rill project partitions
List partitions for a model
Usage:
rill project partitions [] [flags]
Flags:
--project string Project Name
--path string Project directory (default ".")
--model string Model Name
--pending Only fetch pending partitions
--errored Only fetch errored partitions
--local Target locally running Rill
--page-size uint32 Number of partitions to return per page (default 50)
--page-token string Pagination token
```
If running locally, you will need to add the `--local` flag to the command.
```bash
rill project partitions model_name [--local]
KEY (10) DATA EXECUTED ON ELAPSED ERROR
---------------------------------- ----------- ---------------------- --------- -------
ff7416f774dfb086006d0b4696c214e1 {"num":0} 2024-11-12T22:48:49Z 95ms
...
```
### Refreshing Partitions via the CLI
:::note Incremental not enabled
If you try to refresh a partition using the following command on a partitioned but not incremental model, you will experience the following error:
```
rill project refresh --model [--local] --partition ff7416f774dfb086006d0b4696c214e1
Error: can't refresh partitions on model "model_name" because it is not incremental
```
:::
You will need to enable [incremental modeling](/developers/build/models/incremental-partitioned-models) in order to individually refresh a partition.
---
###### Optimize Your Models
---
title: Optimize Your Models
sidebar_label: Performance Optimization
sidebar_position: 45
---
Model performance is critical for maintaining responsive dashboards and ensuring users have access to the most current data. This guide covers strategies for optimizing your Rill models to deliver fast query results while keeping data fresh and up-to-date.
By following these best practices, you can create models that provide both speed and accuracy, delivering insights when your users need them most.
## Local Development / Rill Developer
As discussed in the [templating section](/developers/build/models/templating), there are a few key recommendations to increase model performance in Rill Developer:
1. [Limiting the source data](/developers/build/models/templating#applying-a-one-week-sample-to-the-source-bucket-for-local-development) to a smaller time range (e.g., one week's worth of data instead of the full year)
2. Creating smaller models for development by [applying a raw limit](/developers/build/models/templating#example-conditional-sql-limiting-dev-rows), which will then serve as the starting point for your actual downstream models/modeling
3. [Applying a filter](/developers/build/models/templating#example-leveraging-variables-to-apply-a-filter-and-row-limit-dynamically-to-a-model) to the data to work with only a subset of data
## Model Performance
### Consider which models to materialize
By default, models will be materialized as views (in DuckDB). This allows for a dynamic and highly interactive experience when modeling, such as keystroke-by-keystroke profiling. However, since views are logical in nature, as the complexity and size of your data models continue to grow (especially if the underlying data is very large), this can start to significantly impact performance as these complex queries will need to be continuously re-executed along with a number of profiling queries that the Rill runtime will send in the backend.
In such scenarios, we recommend materializing these models as tables. However, there are some tradeoffs to consider:
- **Pros:** Materializing a model will generally ensure significantly improved performance for downstream dependent models and dashboards.
- **Cons:** Enabling materialization for a model can severely impact or break the "keystroke-by-keystroke" experience and these models may also take longer to update (because the results are being written to a table vs. remaining a view). It can also lead to _degraded_ performance for very specific operations, such as when you need to perform cross joins.
:::tip Materialize models powering dashboards
We strongly recommend materializing final models that are being used directly in dashboards to ensure this data is served more quickly.
:::
### Materialization
Model materialization is something to consider when creating intermediate models. Other than [source models](/developers/build/models/source-models), intermediate models are not, by default, materialized and are views in your underlying database engine. There are some pros and cons to enabling it during the development process.
```sql
-- model.sql
-- @materialize: true
```
```yaml
# model.yaml
materialize: true
```
The pros include improved performance for downstream models and dashboards, especially with complex logic and/or large data sizes. Some cons are certain edge cases like cross joins might have a degraded keystroke-by-keystroke experience, and materialized models are billable.
If you are seeing degraded performance, the first recommendation you'll hear from us is to materialize the metrics-powered model.
### Default Model Materialization
If you want, you can change the default behavior of all models in Rill by setting the default model behavior in the rill.yaml file.
```yaml
models:
materialize: true
```
To override this on a per-model basis, simply set the specific model.sql to false.
```sql
-- @materialize: false
```
:::info To materialize or not to materialize?
There are both pros and cons to materializing your models.
- Pros can include improved performance for downstream models and dashboards, especially when the SQL is complex and/or the data size is large. We generally recommend _materializing_ final models that power dashboards.
- Cons can include a degraded keystroke-by-keystroke modeling experience or for specific edge cases, such as when using cross joins.
If unsure, we would generally recommend leaving the defaults and/or [reaching out](/contact) for further guidance!
:::
## Query Optimization
Query optimization is crucial for maintaining high performance and efficiency, especially when working with data-intensive applications. As Rill dashboards are powered by [OLAP engines](/developers/build/connectors/olap), designed for analytical queries, ensuring that our queries are well-optimized can help maximize the responsiveness and speed of our dashboards. There are also additional potential second-order benefits to optimizing queries in Rill, such as improving ingestion times, how long it takes to build models, how resource-intensive it is to build models, how fast profiling queries run, and more.
### Use appropriate data types and avoid casting when possible
Casting can be expensive, especially when the underlying models are views and not [materialized](#consider-which-models-to-materialize) as a table. For example, if a timestamp column is actually incorrectly typed as a string, then for timeseries charts, Rill ends up having to iterate across each row to try to infer the timestamp and a lot of time parsing has to occur. Similarly, for incorrectly typed or cast columns that are used in calculations, the calculations will have to be constantly looped through, which can be both inefficient and expensive over time (and simply make everything slower).
Similarly, choosing the right data type for each column is also important. Smaller data types, when applicable, consume less memory and can improve query performance. For example, use `INT` instead of `BIGINT` if your data range permits.
### Select the columns you need and avoid `SELECT *` when possible
Because most [OLAP databases](/developers/build/connectors/olap) store data in a columnar format, including [DuckDB](/developers/build/connectors/olap/duckdb), selecting only the columns that you need during the modeling phase ensures that DuckDB will only ingest and store the data _it actually needs_ (speeding up model build times and reducing footprint). Furthermore, columnar formats are optimized for analytical queries, so by selecting only the columns that you need (instead of a blanket `SELECT *`), this will help to minimize data processing times and improve the query execution speed.
### Consider sorting your data by an appropriate timestamp column
Generally speaking, if possible, it is recommended to make sure that your upstream data is relatively well organized and/or sorted by timestamp before being ingested into Rill. This helps to ensure that timeseries queries are efficient when they execute against resulting models and can result in an order of magnitude difference in query performance. This can also help improve the effectiveness of filters by reducing I/O.
:::info When to sort vs. not to sort?
Sorting, especially in DuckDB, _can also be computationally intensive_ and most input data is generally sorted enough (by time). If the data ingested is completely unsorted or sorted by a different, non-timestamp column, it could be worth the computational overhead to sort by a timestamp column (especially if used in a dashboard). If you're unsure, please feel free to [reach out](/contact) and we'd be happy to help you assess the best path forward!
:::
### Use joins efficiently
Plan your joins carefully, especially when working with large datasets. Most [OLAP engines](/developers/build/connectors/olap), DuckDB included, will optimize join operations, but ensuring the join keys are well chosen and considering the size of the datasets being joined can reduce processing time. For example, if you're looking to perform a cross or cartesian join across a very wide table, be sure it's necessary as it can otherwise explode the size of your result set.
### Apply filters early and use WHERE clauses wisely
When possible, it can be good practice to apply filtering early in your queries with `WHERE` clauses to reduce the amount of data being processed in subsequent steps (or downstream models/queries). This can both help to reduce the amount of data being scanned and, given the columnar nature of most [OLAP engines](/developers/build/connectors/olap), significantly speed up queries.
### Optimize your subqueries to leverage joins or CTEs when possible
Subqueries can very often prove to be inefficient and result in suboptimal query execution plans. When possible, it is generally better practice to rewrite subqueries as joins or use Common Table Expressions (CTEs) to make them more readable and potentially more efficient.
### Rather than UNION, consider using UNION ALL when possible
Depending on the [OLAP engine](/developers/build/connectors/olap), `UNION` can be a very expensive operation and much more computationally intensive than `UNION ALL`. For example, when using [DuckDB](/developers/build/connectors/olap/duckdb), a `UNION` will require performing full duplicate eliminations _across all columns_ while a `UNION ALL` will simply concatenate the tables together. If a concatenation is sufficient (for the query), this will be both much quicker and significantly less resource intensive for the query to complete.
## Time Series Transformation
If your time series column is quite granular, this may affect your dashboards as the grain will define how granular the dashboards can be viewed. Instead of handling this in the metrics view by adding a `smallest_time_grain` key, you can use the modeling layer to roll up your data.
:::note Query-time vs Model processing
There are benefits to pre-processing the data in the model layer, but for some quick processing, this can be done in the metrics view.
**Query-time processing** (in metrics views):
- Flexible and dynamic
- No storage overhead
- Slightly slower for complex calculations
**Model-level processing** (in SQL models):
- Pre-computed and optimized
- Faster query performance
- Requires model refresh for updates
:::
### DuckDB Time Functions
DuckDB provides a comprehensive toolkit for temporal data manipulation:
- **`DATE_TRUNC`**: Normalize timestamps to consistent intervals (day, week, month, quarter, year)
- **`EXTRACT`**: Extract specific time components (year, quarter, month, day of week, hour)
- **`LAG/LEAD`**: Reference prior or future rows for period-over-period comparisons
- **`DATE_ADD/DATE_SUB`**: Perform date arithmetic for dynamic time ranges
- **`STRFTME`**: Extract strings from a time column.
For comprehensive documentation on all available time functions, see the [DuckDB time functions documentation](https://duckdb.org/docs/stable/sql/functions/timestamp.html).
:::tip not using DuckDB?
Each engine has slightly different functions and syntax for rolling up your data, see your OLAP engine's documentation for more examples.
:::
### Time Aggregation (Roll-ups)
Roll-ups aggregate granular events into coarser intervals. For example, if your data arrives hourly but daily analysis suffices:
```sql
SELECT DATE_TRUNC('day', timestamp_column) AS time_series_column,
...
FROM your_model
```
---
###### Source Models
---
title: Source Models
sidebar_label: Source Models
sidebar_position: 10
---
After [creating a connector to your data source](/developers/build/connectors/data-source), you'll need to create a model to bring that data into Rill. This can be implemented as either a SQL model with [defined connector parameters](/developers/build/models/sql-models#specifying-the-data-source-connector) or as a YAML configuration file. This guide focuses on YAML-based source models, which are auto-generated when using the UI.
```yaml
# Model YAML
# Reference documentation: https://docs.rilldata.com/reference/project-files/models
type: model
materialize: true
connector: snowflake
sql: |
select * from database_name.schema_name.table_name
```
### YAML Structure
The YAML configuration file contains several key parameters:
- **`type: model`**: Explicitly defines the file type. While Rill automatically detects the file type based on the parent folder, this parameter provides explicit definition.
- **`connector`**: Defines the connector type used to create the model (e.g., `bigquery`, `athena`, `snowflake`, etc.).
- **`sql`**: The actual SQL query to be executed. When nested under `dev:`, the query runs in the Rill Developer environment.
- **`dev`**: Configuration for development mode. Rill Developer runs in dev mode by default, but when deployed to Rill Cloud, the root-level SQL configuration executes. See [Environment Templating](/developers/build/models/templating) for more information.
## Retry Configuration
By default, a model will retry if the initial load fails. This helps ensure reliable data processing by automatically retrying failed operations. The default retry settings are:
The default `if_error_matches` values are exactly:
`".*OvercommitTracker.*"`, `".*Bad Gateway.*"`, `".*Timeout.*"`, and `".*Connection refused.*"`.
If you set `retry.if_error_matches`, it overrides these defaults rather than appending to them.
```yaml
retry:
attempts: 3
delay: 5s
exponential_backoff: true
if_error_matches:
- ".*NetException.*" # overrides defaults
```
You can customize the retry behavior to better suit your specific needs. For example, you might want to increase the number of attempts for critical models, adjust the delay between retries, or only retry on specific error types. Use the following configuration in your source YAML:
```yaml
retry:
attempts: 5
delay: 10s
exponential_backoff: true
if_error_matches:
- ".*OvercommitTracker.*"
- ".*Bad Gateway.*"
- ".*Timeout.*"
- ".*Connection refused.*"
- ".*NetException.*"
```
## Examples
### BigQuery Model
```yaml
# Model YAML
# Reference documentation: https://docs.rilldata.com/reference/project-files/models
type: model
materialize: true
connector: bigquery
dev:
sql: select * from project_id.dataset_name.table_name limit 10000
sql: select * from project_id.dataset_name.table_name
```
### Snowflake Model
```yaml
# Model YAML
# Reference documentation: https://docs.rilldata.com/reference/project-files/models
type: model
materialize: true
connector: "snowflake"
dev:
sql: select * from database_name.schema_name.table_name limit 10000
sql: select * from database_name.schema_name.table_name
```
### S3 Model
```yaml
# Model YAML
# Reference documentation: https://docs.rilldata.com/reference/project-files/models
type: model
materialize: true
connector: "duckdb"
dev:
sql: |
select * from read_csv('s3://my-bucket/path/to/file.csv', auto_detect=true, ignore_errors=1, header=true) limit 10000
sql: |
select * from read_csv('s3://my-bucket/path/to/file.csv', auto_detect=true, ignore_errors=1, header=true)
```
For more information, see our [model reference documentation](/reference/project-files/models)!
## Next Steps
Rill provides automatic refresh capabilities for your source models at specified intervals to maintain data currency. This functionality enables you to establish scheduled data ingestion without manual intervention, ensuring your analytics dashboards remain current with the latest information from your data sources. For additional details, see [Scheduled Refreshes](/developers/build/models/data-refresh).
After validating your source model configuration and confirming the data preview appears correct, you can move forward to create your first metrics view. If no additional data transformations are needed, you can choose [Generate Metrics View with AI](/developers/build/metrics-view) from the top-right corner of the interface to initiate Rill's AI-powered dashboard generation.
---
###### SQL Models
---
title: SQL Models
description: Create models from source data and apply SQL transformations
sidebar_label: SQL Models
sidebar_position: 05
---
## Adding a data model
Add a new data model by either clicking 'model' in the 'Add' menu or selecting the '...' in any connector view or existing model. When you add a model, a code definition will automatically be created as a `.sql` file in the `models` folder of your Rill project.
You can also create a model outside of the application and add it to Rill by placing a `.sql` file in the `models` directory containing a DuckDB SQL `SELECT` statement. Rill will automatically detect and parse the model the next time you run `rill start`.
## Annotating your models with properties
In most cases, objects are represented in Rill as YAML files. Models are unique in that any `.sql` file can be considered a model resource in Rill, representing a SQL transformation that you would like to perform using a set of inputs and outputting a view or table (depending on the materialization type). For most other resources, available properties can be set directly via the corresponding YAML file. In the case of a model SQL file though, configurable properties should be set by annotating the top of the file using the following syntax:
```sql
-- @property: value
select * from users
```
We will cover different available configurable properties in the sections below.
### Marking your model SQL file as a model resource type
By default, any new model that is created in a Rill project will populate a corresponding `.sql` file representing the model. Similarly, a `.sql` file that is directly created in the project directory will also be _automatically assumed_ by Rill to be a model by default. Therefore, it is not necessary to annotate the model resource with the `type` property.
For consistency or documentation purposes, if you'd like to annotate your model resource as well with the `type` property, you can do so by adding the following to the top of your `.sql`:
```sql
-- @type: model
select * from orders
```
### Specifying the Data Source Connector
The `@connector` annotation explicitly defines which data source connector to use when executing the SQL query. This parameter is essential when working with multiple connectors of the same type, as it determines which connection credentials and source path to utilize. By default, query results are written to the project's default [OLAP engine](/developers/build/connectors/olap#olap-engines).
```sql
-- @connector: bigquery
SELECT * FROM project_id.dataset_name.table_name
```
```sql
-- @connector: postgres
SELECT * FROM mydb.public.users
```
For projects configured with [multiple OLAP engines](/developers/build/connectors/olap/multiple-olap), you can specify both input and output connectors for a SQL model. Nested parameter syntax uses dot notation (e.g., `output.connector`).
```sql
-- @connector: clickhouse
-- @output.connector: clickhouse
select from clickhouse_table
```
## Working with Pivots
Pivots deserve their own section, as using the [Pivot](https://duckdb.org/docs/sql/statements/pivot) statement while modeling requires special consideration. Notably, there are a few existing DuckDB limitations to consider:
- DuckDB's [SQL to JSON serializer](https://duckdb.org/docs/extensions/json.html#serializing-and-deserializing-sql-to-json-and-vice-versa) doesn't support `PIVOT` without the `IN` [filter](https://duckdb.org/docs/sql/statements/pivot#in-filter-for-on-clause)
- DuckDB doesn't support creating views based on `PIVOT` without an `IN` filter (and all models are materialized as views by default in Rill)
Fortunately, there are workarounds available to address these limitations.
#### Passing the `IN` Filter with Your `PIVOT` Statement
If you know the exact values you want to pivot on, you can specify them using an `IN` filter with your `ON` clause. For example, instead of:
```sql
PIVOT table_name ON column_name USING SUM(measure)
```
You can use the following `PIVOT` statement:
```sql
PIVOT table_name ON column_name IN (value_a, value_b, value_c) USING SUM(measure)
```
---
###### Staging Models
---
title: Staging Models
description: When the source ingestion is not directly supported, use staging models
sidebar_label: Staging Models
sidebar_position: 35
---
As the name suggests, a staging model is one that uses a staging platform to ingest data from one source to another output that is not directly supported. An example of this is ingesting from Snowflake to ClickHouse. In order to support our users' varying setups, we've devised staging models.
## When to use a Staging Model?
Staging models are required for situations where the input and output types are not directly supported. However, by using a staging platform that is supported by both platforms, it allows users to temporarily write data to the staging platform and ingest directly to the output platform.
:::note Supported Paths
Staging Models are in ongoing development. While we do have support for the following, please [reach out to us](/contact) if you have any specific requirements.
- Snowflake → S3 → ClickHouse
- BigQuery → GCS → ClickHouse
:::
### Example
Let's expand further on the Snowflake to ClickHouse example.
```mermaid
sequenceDiagram
Snowflake->>Staging (S3): write data to temporary Staging
Staging (S3)->>ClickHouse: write temporary data to ClickHouse
Staging (S3) ->> Staging (S3): remove temporary data from Staging
```
In the above example, during the ingestion from Snowflake to ClickHouse, we use the temporary staging table in S3 to write from Snowflake to S3, then from S3 to ClickHouse. Once this procedure is complete, we clear the temporary data from S3.
### Sample YAML:
```yaml
# Use DuckDB to generate a range of days from 1st Jan to today
partitions:
connector: duckdb
sql: SELECT range as day FROM range(TIMESTAMPTZ '2024-01-01', now(), INTERVAL 1 DAY)
# Don't reload previously ingested partitions on every refresh
incremental: true
# Query Snowflake for all events belonging to the current partition
connector: snowflake
sql: SELECT * FROM events WHERE date_trunc('day', event_time) = '{{ .partition.day }}'
# Since ClickHouse can't ingest from Snowflake or vice versa, we use S3 as a temporary staging connector
stage:
connector: s3
path: s3://bucket/temp-data
# Produce the final output into ClickHouse, requires a clickhouse.yaml connector defined.
output:
connector: clickhouse
```
---
###### Model Environment Templating
---
title: Model Environment Templating
description: Dev/Prod Setup
sidebar_label: Environment Templating
sidebar_position: 40
---
Along with [connector templating](/developers/build/connectors/templating), you can also optimize your development environment for performance and cost-effectiveness at the model layer. This approach is particularly useful when you don't have a separate development database or file system, as it allows you to limit the amount of data processed in source models.
## Why Use Templating?
Templating serves several important purposes in your data workflow:
- **Cost Management**: Avoid running expensive queries on production data during development
- **Performance in dev**: Optimize development environment performance by limiting data volume
- **Testing Safety**: Test your models and transformations without affecting production data
:::note Rill Developer is a dev environment
Unless explicitly defined, Rill Developer will use a `dev` environment. If you want to emulate production locally, you can do so via `rill start --environment prod`.
:::
## Separate Dev and Prod SQL Queries
Another common templating pattern is using the `dev:` partition in your model definitions. This tells Rill to use a different data source or query when running in development mode, typically with a smaller dataset or different data location.
### Example: Ingest Data from Specific Folder Path in Dev
```yaml
# Model YAML
# Reference documentation: https://docs.rilldata.com/reference/project-files/models
type: model
connector: "duckdb"
dev:
sql: "select * from read_parquet('gs://rilldata-public/github-analytics/Clickhouse/2025/06/commits_2025_06.parquet')"
sql: "select * from read_parquet('gs://rilldata-public/github-analytics/Clickhouse/*/*/commits_*.parquet')"
```
:::info Why is the connector type duckdb and not s3 or gcs?
In this case, we are using the [embedded DuckDB engine](/developers/build/connectors/olap/duckdb) to execute a [SELECT](https://duckdb.org/docs/sql/statements/select.html) statement while leveraging DuckDB's native [read_parquet](https://duckdb.org/docs/data/parquet/overview.html) function. Therefore, the `connector` type ends up being `duckdb` instead of `s3`. For more details, see our [Source YAML](/reference/project-files/sources) reference documentation.
:::

In this example:
- **Development**: Reads from a single file (`2025/06/commits_2025_06.parquet`) for faster processing
- **Production**: Reads from all files in the folder using the `/*/*/` wildcard pattern; you can also use `/**/` if you're unsure of the number of directories
## Inline SQL Templating
You can also use Go template syntax directly within your SQL queries to conditionally include or exclude parts of your query based on the environment.
### Example: Conditional SQL Limiting Dev Rows
```sql
select
*
from read_csv('gs://your-bucket/folder/**/file.csv', auto_detect=true, ignore_errors=1, header=true)
{{ if dev }}
limit 10000
{{ end }}
```
```sql
select
*
from read_csv('gs://your-bucket/folder/**/file.csv', auto_detect=true, ignore_errors=1, header=true)
{{ if dev }}
where column = 'value'
{{ end }}
```
In this example:
- **Development**: Adds a where/limit clause, filtering the data by a specific value to restrict the dataset size
- **Production**: No limits
### Example: If/Else Conditions
```sql
select
*
from read_csv('gs://your-bucket/folder/**/file.csv', auto_detect=true, ignore_errors=1, header=true)
{{ if dev }}
limit 10000
{{ else }}
where column = 'value'
{{ end }}
```
In this example:
- **Development**: Adds a `LIMIT 10000` clause to restrict the dataset size
- **Production**: Applies a where clause to filter the data in production
### Applying a One-Week Sample to the Source Bucket for Local Development
In another example, let's say we had an [S3](/developers/build/connectors/data-source/s3) source defined that happens to be reading a very large amount of parquet data. Following best practices, we'll want to read in a subset of this source data for local modeling in Rill Developer rather than using the full dataset for development purposes. Furthermore, we'll make the assumption that the upstream data is not partitioned and thus the S3 bucket is not partitioned (where we could then simply filter the `path` by using a glob pattern potentially in conjunction with [environment-specific logic](/developers/build/models/templating)). So what can we do?
Fortunately, we can leverage DuckDB's ability to read from S3 files directly and _apply a filter post-download_ using templating logic in the SQL. In this case, because there is an existing `updated_at` timestamp column, we can use it to filter and retrieve only one week's worth of data. For example, our `source.yaml` file may end up looking something like this:
```yaml
type: model
connector: "duckdb"
sql: >
SELECT * FROM read_parquet('s3://bucket/path/*.parquet')
{{ if dev }}
where updated_at > CURRENT_DATE - INTERVAL 7 DAY
{{ end }}
```
:::tip Dynamic dates
Depending on the OLAP engine of your project, you can set the dates dynamically using `CURRENT_DATE` (DuckDB) or `now()` (ClickHouse) and subtract days.
:::
### Example: Leveraging Variables to Apply a Filter and Row Limit Dynamically to a Model
Our last example will highlight how the same templating concepts can be applied with [variables](/developers/build/connectors/credentials#setting-credentials-for-rill-developer). In this case, we have a source dataset about horror movies that came out in the past 50 years, which includes various characteristics, attributes, and metrics about each horror movie as separate columns.
For example, we know the release date, how many people saw a movie, what the budget was, its popularity, the original language of the movie, the genres, and much more.
Let's say that we wanted to apply a filter on the resulting model based on the `original_language` of the movie and also limit the number of records that we retrieve, which will be based on the `language` and `local_limit` variables we have defined. Taking a quick look at our project's `rill.yaml` file, we can see the following configuration (to return only English movies and apply a limit of 5):
```yaml
env:
local_limit: 5
language: "en"
```
Furthermore, our `model.sql` file contains the following SQL:
```sql
SELECT * FROM {{ ref "snowflake" }}
WHERE original_language = '{{ .env.language }}'
{{if dev}} LIMIT {{ .env.local_limit }} {{end}}
```
:::warning When applying templated logic to model SQL, make sure to leverage the `ref` function
If you use templating in SQL models, you must replace references to tables/models created by other sources or models with `ref` tags. This ensures that the native Go templating engine used by Rill is able to resolve and correctly compile the SQL syntax during runtime (to avoid any potential downstream errors).
:::
If we simply run Rill Developer using `rill start`, our model will look like the following (this will also reflect our data model in production, i.e., Rill Cloud, after we've [pushed the changes for the project to GitHub](/developers/deploy/deploy-dashboard)):

**Now**, just to illustrate what a local override might look like, let's say we stop Rill Developer and then restart Rill via the CLI with the following command:
```bash
rill start --env language="ja" --env local_limit=100
```
Even though we have defaults set in `rill.yaml` (and this will be used by any downstream models and dashboards on Rill Cloud), we will instead see these local overrides come into effect with our templated logic to return Japanese movies and the model limit is now 100 rows.

---
#### Structure
---
###### Structure your project
---
title: Structure your project
sidebar_label: Organize your Code Files
sidebar_position: 00
---
After creating your initial set of sources, models, and dashboards, you may have noticed the following _native_ folders that exist in your Rill project directory:
- [Models](/reference/project-files/models)
- [Metrics Views](/reference/project-files/metrics-views)
- [Dashboards](/reference/project-files/explore-dashboards)
By default, any new sources, models, metrics views, and dashboards will be created in their respective native folders. However, this does not necessarily have to be the case, and Rill Developer allows for a flexible project directory structure, including nested folders or even storing objects in non-native folders. This is a powerful feature that allows you, as a developer, to organize your project to meet your team's specific needs.
## Adding new resources or parent folders
Within Rill Developer, from the left-hand side (file explorer), you should be able to click on the `Add` button to add a new resource, such as a new source, model, or dashboard. Furthermore, you will also have the ability to add a new parent folder to store groups of resources (which can be mixed). If you choose to add a new folder, you should see the folder structure reflected when you check the project directory via the CLI.

:::warning Make sure to include the `type` property
For backward compatibility purposes, any resource that belongs in the `sources`, `models`, and `dashboards` native folders is assumed to be a source, model, or dashboard respectively (including nested folders that belong within a native folder).
However, if you'd like to create a resource outside one of these native folders, make sure to include the `type` property in the resource definition, or Rill will not be able to properly resolve the resource type! For more details, see our [reference documentation](/reference/project-files/rill-yaml).
:::
## Navigating Upstream / Downstream Objects

When selecting between a source, model, metrics view, and dashboard, you can view the upstream/downstream objects to the current view. For example, if you are selecting a metrics view, you can see all of the dashboards (in a dropdown) that are built on the metrics view. Likewise, if your model references several sources, these will be available to select.
## Moving resources within your project
From the UI, within the file explorer, you should be able to drag resources/objects around and move them to or from folders as necessary.
:::info Using the CLI
For developers who prefer to use the CLI, the project structure can still be controlled or adjusted directly via the CLI or your preferred IDE (e.g., VS Code).
:::
## Adding a nested folder
Rather than creating a parent folder and moving it into another folder manually, there is a shortcut to create a nested folder by directly hovering over an existing folder in the file explorer, clicking on the "triple dots," and selecting `New folder`.

---
##### "AI Configuration"
---
title: "AI Configuration"
description: "Configure AI instructions for your Rill project"
sidebar_label: "AI Configuration"
sidebar_position: 55
---
# AI Configuration
Rill's AI capabilities, including [AI Chat](/guide/ai/ai-chat) and the [MCP Server](/guide/ai/mcp), rely on context to provide accurate and relevant answers. You can provide additional context using the `ai_instructions` field in your project configuration files.
LLMs give their best results when they have good context. For a conversation with Rill Data, this means things like clarifying project-specific terms, routing questions to the correct metrics view, or defining business rules. Rather than expecting the user to provide this context every time, you can add `ai_instructions` to your project. This adds the context automatically for every conversation.
There are two places to add `ai_instructions`:
1. **`rill.yaml`**: Project-wide instructions that apply to all queries across your entire project.
2. **`.yaml`**: Metrics view-specific instructions for individual dashboards.
## Automatic Context Inclusion
In addition to `ai_instructions`, Rill automatically includes the following in the AI context:
- **Measure and dimension descriptions**: Any `description` fields you add to measures and dimensions in your metrics view YAML files are automatically included in the AI context. This helps the AI understand what each metric or dimension represents without requiring you to duplicate that information in `ai_instructions`.
- **Metrics view metadata**: The metrics view name, display name, and description are included to help route questions to the correct dashboard.
This means you can document your measures and dimensions directly in your metrics view YAML, and that documentation will be available to the AI automatically.
## Project-Level Instructions ([`rill.yaml`](/developers/build/project-configuration))
Use the `ai_instructions` field in `rill.yaml` to provide information that is **unique to your project**. This helps the AI agent deliver more relevant and actionable insights tailored to your specific needs.
**What to include:**
- Guidance on which metrics views are most important or should be prioritized for your project
- Any custom business logic, definitions, or terminology unique to your data or organization
- Preferences for aggregations, filters, or dimensions that are especially relevant to your use case
- Specific business context that helps the AI understand your domain
**Example:**
Here's an example of how you might configure `ai_instructions` in your `rill.yaml` to provide project context, metrics routing, and business definitions:
```yaml
ai_instructions: |
# Project Context
This project tracks e-commerce metrics for our multi-brand retail business.
# Metrics View Routing
- For questions about overall sales, revenue, or order volume → use `company_sales_metrics`
- For questions about customer behavior, retention, or cohorts → use `customer_analytics`
- For questions about product performance or inventory → use `product_metrics`
- For questions about marketing campaigns or attribution → use `marketing_performance`
- For questions about fulfillment, shipping, or logistics → use `operations_metrics`
# Business Rules & Definitions
- "Revenue" always refers to net revenue (after returns and discounts)
- "Conversion rate" is calculated as orders/sessions, not users
- Our fiscal year starts in February, not January
- "Active customer" means a purchase within the last 90 days
- Weekend traffic patterns are anomalous due to our B2B focus
# Company Acronyms
- GMV = Gross Merchandise Value
- AOV = Average Order Value
- ROAS = Return on Ad Spend
- SKU = Stock Keeping Unit
- NDR = Net Dollar Retention
- CLTV = Customer Lifetime Value
# Known Data Quirks
- Mobile web data before March 2024 is incomplete due to tracking migration
- European region data excludes VAT (use `revenue_with_vat` dimension if needed)
- Refunds are processed with a 2-3 day delay, so recent data may shift
```
## Metrics View-Level Instructions ([`.yaml`](/developers/build/metrics-view/what-are-metrics-views))
You can provide context and instructions for AI tools interacting with a specific metrics view using the `ai_instructions` field in the metrics view's YAML file. This is useful for clarifying specific metrics, dimensions, or data quirks that apply only to that specific view.
:::tip Use descriptions for measure and dimension documentation
Instead of (or in addition to) adding definitions in `ai_instructions`, you can use the `description` key in the measure and dimension definitions in your metrics view YAML to document what each metric or dimension represents. These descriptions are automatically included in the AI context, making your metrics view self-documenting.
:::
**Example:**
```yaml
ai_instructions: |
# Analysis Guidance
- When analyzing "Revenue", always breakdown by "Region" to see currency impacts.
- For questions about user growth, prioritize the "monthly_active_users" measure over "daily_active_users".
- When comparing time periods, account for the fact that data for the "Legacy Plan" is static and will not update after Dec 2023.
# Data Context
- Mobile web data before March 2024 is incomplete due to tracking migration.
- Refunds are processed with a 2-3 day delay, so recent data may shift.
- Weekend traffic patterns are anomalous due to our B2B focus.
```
## Visualization Tips
When using the [Rill MCP Server](/guide/ai/mcp) with external AI clients like Claude, you can provide specific instructions on how to visualize data. Since the MCP server returns structured data, the AI client is responsible for rendering it.
:::note Visualization tips affect all AI interactions
Visualization instructions added to `rill.yaml` will affect both [Rill Chat](/guide/ai/ai-chat) responses and external AI clients via the MCP Server. If you only want visualization tips to apply to external AI clients (like Claude Desktop), consider adding them to your client-specific configuration files instead:
- **Claude Desktop**: Add to `claude_desktop_config.json` or `Claude.md` in your project
- **Cursor**: Add to `.cursorrules` or `AGENT.md` in your project
- **Other AI clients**: Check your client's documentation for where to add custom instructions
This way, visualization formatting will only apply when using external clients, while Rill Chat maintains its default formatting.
:::
You can add instructions to your `rill.yaml` to guide the AI in presenting data more effectively (note that this will affect both Rill Chat and MCP clients):
```yaml
ai_instructions: |
# Visualization Guidelines
- When presenting time series data, use sparklines or trend indicators (e.g. 📈/📉) to show direction.
- For comparisons, clearly state the percentage change and absolute difference.
- Use bar charts for categorical comparisons when there are fewer than 10 categories.
- When showing tables, always include a header row and align numeric columns to the right.
# Example Formatting
- Bar Charts using block characters:
Q1 ████████░░ 411
Q2 ██████████ 514
Q3 ██████░░░░ 300
Q4 ████████░░ 400
- Horizontal progress bars: Project Progress:
Frontend ▓▓▓▓▓▓▓▓░░ 80%
Backend ▓▓▓▓▓▓░░░░ 60%
Testing ▓▓░░░░░░░░ 20%
- Using different block densities: Trends:
Jan ▁▂▃▄▅▆▇█ High
Feb ▁▂▃▄▅░░░ Medium
Mar ▁▂░░░░░░ Low
- Sparklines with Unicode Basic sparklines:
Stock prices: ▁▂▃▅▂▇▆▃▅▇
Website traffic: ▁▁▂▃▅▄▆▇▆▅▄▂▁
CPU usage: ▂▄▆█▇▅▃▂▄▆█▇▄▂
- Trend indicators:
AAPL ▲ +2.3%
GOOG ▼ -1.2%
MSFT ► +0.5%
TSLA ▼ -3.1%
- Simple trend arrows:
Sales ↗️ (+15%)
Costs ↘️ (-8%)
Profit ⤴️ (+28%)
```
---
##### Alerts
---
title: Alerts
description: Define alerts as code for automated monitoring and notifications
sidebar_label: Alerts
sidebar_position: 45
---
## Overview
Alerts in Rill allow you to monitor your data and receive notifications when specific conditions are met. While alerts can be created through the UI, defining them as code in YAML files provides version control, reproducibility, and the ability to manage complex alerting logic programmatically.
When you create an alert via a YAML file, it appears in the UI marked as `Created through code`.
:::tip Using live connectors?
If you're using [live connectors](/developers/build/connectors/olap) (ClickHouse, Druid, Pinot, StarRocks, etc.), **alerts are your primary tool for data quality monitoring**. Since live connectors don't create local models, [data quality tests](/developers/build/models/data-quality-tests) won't run. Use alerts instead to validate your data on a schedule.
:::
## Alert Structure
An alert YAML file has the following core components:
```yaml
type: alert
display_name: My Alert Name
description: A brief description of what this alert monitors
# When to check the alert
refresh:
cron: "0 * * * *" # Every hour
# What data to check
data:
sql: SELECT * FROM my_model WHERE condition_is_bad
# Where to send notifications
notify:
email:
recipients:
- team@example.com
```
## Scheduling Alerts
The [`refresh`](/reference/project-files/alerts#refresh) property defines when and how often the alert runs.
### Cron Schedule
Use standard `cron` expressions to define the schedule:
```yaml
refresh:
cron: "0 * * * *" # Every hour
time_zone: "America/New_York" # Optional timezone
```
### Interval-Based Monitoring
Use `intervals` when you need to check data across multiple time windows, such as validating metrics for each hour or day. This is useful for time-series monitoring where you want to ensure data quality across a rolling window of time periods. Interval-based monitoring is more flexible than simple cron schedules when you need to check multiple historical periods on each evaluation.
```yaml
refresh:
cron: "5 * * * *" # 5 minutes past each hour
intervals:
duration: PT1H # 1 hour intervals
limit: 24 # Check last 24 intervals
check_unclosed: false
```
## Data Sources
Alerts support multiple data source types to query your data.
### SQL Query
Execute raw SQL against your models:
```yaml
data:
sql: |
SELECT *
FROM orders
WHERE created_at < NOW() - INTERVAL '24 hours'
AND status = 'pending'
```
The alert triggers when the query returns **any rows**.
### Metrics SQL
Use `metrics_sql` when you want to query a [metrics view](/developers/build/metrics-view) using its defined dimensions and measures, rather than writing raw SQL against the underlying model. This approach leverages the metrics view's security policies and allows you to reference measures and dimensions by name. For details on the `metrics_sql` syntax, see [Metrics SQL APIs](/developers/build/custom-apis/metrics-sql).
```yaml
data:
metrics_sql: |
SELECT *
FROM sales_metrics
WHERE total_revenue < 1000
```
### Custom API
Use a custom API when you want to reuse complex query logic that's already defined as a [Custom API](/developers/build/custom-apis) in your project. This approach is useful for sharing validation logic between alerts and other integrations, or when you need to pass dynamic arguments to your alert queries.
```yaml
data:
api: my_custom_validation_api
args:
threshold: 100
date_range: "7d"
```
### Resource Status
Monitor the health of your Rill resources to catch pipeline failures and reconciliation errors. This is useful for monitoring pipeline health and catching reconciliation failures before they impact downstream processes.
```yaml
data:
resource_status:
where_error: true
```
This triggers when any resource in your project has a reconciliation error.
## Notification Configuration
Configure where and how you receive notifications when alerts trigger. You can send notifications via email, Slack, or both. Notifications are sent when the alert condition is met (when the data query returns rows), and optionally when the alert recovers or encounters evaluation errors.
### Email Notifications
```yaml
notify:
email:
recipients:
- alice@example.com
- bob@example.com
- data-team@example.com
```
### Slack Notifications
Before using Slack notifications, you must [configure the Slack integration](/developers/build/connectors/services/slack) for your project.
```yaml
notify:
slack:
channels:
- "#data-alerts"
- "#engineering"
users:
- "U1234567890" # Slack user IDs
webhooks:
- "https://hooks.slack.com/services/..."
```
### Combined Notifications
Send to multiple destinations:
```yaml
notify:
email:
recipients:
- team@example.com
slack:
channels:
- "#alerts"
```
## Alert Behavior
### Recovery Notifications
Control when you receive notifications about alert state changes. Use `on_recover` to confirm issues are resolved and get peace of mind that problems have been fixed. Use `on_error` to catch alert evaluation failures (e.g., query syntax errors) that prevent the alert from running properly.
```yaml
on_recover: true # Notify when alert recovers
on_fail: true # Notify when alert triggers (default)
on_error: false # Notify on evaluation errors
```
### Re-notification (Snooze)
Control how often you're notified for ongoing issues. This prevents alert fatigue while ensuring ongoing issues aren't forgotten. Instead of receiving notifications on every evaluation cycle, you'll only be re-notified after the specified duration if the alert is still failing.
```yaml
renotify: true
renotify_after: "24h" # Re-notify every 24 hours if still failing
```
## Working Examples
### Data Freshness Alert
This example demonstrates a data freshness check that queries the maximum timestamp from an events model and triggers when data is older than 24 hours. It uses both email and Slack notifications, includes recovery notifications to confirm when data freshness is restored, and implements re-notification every 6 hours to prevent alert fatigue while ensuring ongoing issues are tracked.
```yaml
# alerts/data_freshness.yaml
type: alert
display_name: Data Freshness Check
description: Alert when event data is stale
refresh:
cron: "0 * * * *" # Check every hour
data:
sql: |
SELECT 'Data is stale' AS error_message
FROM (
SELECT MAX(event_timestamp) AS latest_event
FROM events_model
)
WHERE latest_event < NOW() - INTERVAL '24 hours'
notify:
email:
recipients:
- data-ops@example.com
slack:
channels:
- "#data-alerts"
on_recover: true
renotify: true
renotify_after: "6h"
```
### Project Health Monitor
This example monitors the overall health of your Rill project by checking for any resource reconciliation errors. It runs every 10 minutes for rapid detection of pipeline failures, uses the `resource_status` data source to automatically detect errors across all resources, and sends notifications to both Slack and email channels. Recovery notifications ensure you're alerted when issues are resolved.
```yaml
# alerts/project_health.yaml
type: alert
display_name: Project Health Monitor
description: Alert when any resource has a reconciliation error
refresh:
cron: "*/10 * * * *" # Every 10 minutes
data:
resource_status:
where_error: true
notify:
slack:
channels:
- "#rill-alerts"
email:
recipients:
- platform-team@example.com
on_recover: true
```
### Interval-Based Monitoring Example
This example shows how to use interval-based monitoring to validate metrics across multiple time periods. It checks hourly aggregates for the last 24 hours, looking for any hours with zero event counts. The alert runs 5 minutes past each hour to ensure the previous hour's data is complete, and uses the `intervals` configuration to systematically check each hour in the rolling window. This pattern is ideal for time-series data quality monitoring where you need to validate multiple periods on each evaluation.
```yaml
# alerts/hourly_metrics.yaml
type: alert
display_name: Hourly Metrics Check
description: Validate metrics for each hour
refresh:
cron: "5 * * * *" # 5 minutes past each hour
intervals:
duration: PT1H # 1 hour intervals
limit: 24 # Check last 24 intervals
check_unclosed: false
data:
sql: |
SELECT *
FROM hourly_aggregates
WHERE hour_start = DATE_TRUNC('hour', NOW() - INTERVAL '1 hour')
AND event_count = 0
notify:
slack:
channels:
- "#monitoring"
```
## Reference
For the complete specification of all available properties, see the [Alert YAML Reference](/reference/project-files/alerts).
:::note Advanced Properties
For advanced properties like `glob`, `for`, `watermark`, and `timeout`, see the [Alert YAML Reference](/reference/project-files/alerts).
:::
---
##### "Your First Rill Project"
---
title: "Your First Rill Project"
description: "Get started with Rill by understanding what happens when you create a project"
sidebar_label: "Your First Project"
sidebar_position: 1
---
# Your First Rill Project
When you create a new Rill project, you're setting up a complete data pipeline that transforms raw data into interactive dashboards. This guide explains what happens during project initialization and introduces you to the key files you'll work with.
## What Happens When You Create a Rill Project?
A Rill project consists of several data assets that work together to create a dashboard. The data pipeline begins with [connecting](/developers/build/connectors) to your data sources, transforms raw data through [models](/developers/build/models), defines metrics and dimensions in [metrics views](/developers/build/metrics-view), and results in interactive [dashboards](/developers/build/dashboards) for data analysis.
When you create a new Rill project, the following files are automatically generated:
- **`rill.yaml`** - Central configuration hub for your entire project
- **`connectors/.yaml`** - Connector configuration files for the project's default OLAP engine (e.g., `duckdb.yaml`, `clickhouse.yaml`)
- **`.gitignore`** - Git ignore rules for the project
## Initial Project Files
These are the foundational files that Rill creates for you. As you build your project, you'll add many more files including sources, models, metrics views, and dashboards.
### `rill.yaml`
The central configuration file that controls project-wide settings. You rarely need to modify `rill.yaml` when starting out - the defaults work great! This file enables you to set [project-wide defaults](/developers/build/project-configuration#model-defaults), [configure variables](/developers/build/project-configuration#variable-management), define [connector settings](/developers/build/project-configuration#olap-connector), create [test users](/developers/build/project-configuration#testing-security), and establish [security policies](/developers/build/project-configuration#metrics-views-security-policy).
### `connectors/.yaml`
Configuration for your default OLAP engine (DuckDB, ClickHouse, Druid, or Pinot). When starting a blank project, this always defaults to `duckdb.yaml`. This file defines how Rill connects to your analytical database.
Connectors enable Rill to connect to various data sources and OLAP engines. You can configure [data source connectors](/developers/build/connectors/data-source) (like S3, GCS, BigQuery, Snowflake) to ingest data, and [OLAP connectors](/developers/build/connectors/olap) to power your analytics. See our [connectors documentation](/developers/build/connectors) for the full list of supported connections.
### `.gitignore`
Specifies which files and directories should be ignored by Git version control. Rill automatically generates this file with appropriate rules to exclude sensitive files like `.env`, temporary files, and build artifacts from being committed to your repository.
## Preview Mode
Preview mode starts Rill Developer with a dashboard-only interface — no file editor, no code. It's designed for sharing a local Rill instance with stakeholders who don't need the development environment.
```bash
rill start my-project --preview
```

Preview mode sets the application to **read-only**, so dashboards cannot be modified through the UI.
:::tip
For a full team deployment with authentication and access controls, [deploy to Rill Cloud](/developers/deploy/deploy-dashboard).
:::
## Next Steps
Now that you understand the basics, you can:
1. **[Connect to your data](/developers/build/connectors)** - Set up connections to your data sources
1. **[Create your first model](/developers/build/models)** - Transform your raw data
2. **[Build a metrics view](/developers/build/metrics-view)** - Define your metrics and dimensions
3. **[Create a dashboard](/developers/build/dashboards)** - Visualize your data
4. **[Deploy to Rill Cloud](/developers/deploy)** - Deploy your project to Rill Cloud
When you need more control over your project configuration, see our [project configuration guide](/developers/build/project-configuration) for advanced settings and customization options.
---
##### Building Your Data Analytics Pipeline in Rill
---
title: Building Your Data Analytics Pipeline in Rill
sidebar_label: Build
sidebar_position: 0
---
Rill provides a comprehensive platform for building end-to-end data analytics solutions. Once you've connected to your source data or connected to your own OLAP database, you can start building with Rill. Don't forget to check out the [reference documentation!](/reference/project-files)
### What is a Rill project?
- [**Getting Started with your Rill Project**](/developers/build/getting-started) - Understand Rill project structure and configuration
### Connect to your Data
- [**Connectors Overview**](/developers/build/connectors) - Learn about connecting Rill to data sources and OLAP engines
- [**OLAP Engines**](/developers/build/connectors/olap) - Configure DuckDB, ClickHouse, Druid, or Pinot for analytics
- [**Data Sources**](/developers/build/connectors/data-source) - Connect to S3, GCS, BigQuery, Snowflake, and more
- [**Configure Local Credentials**](/developers/build/connectors/credentials) - Set up credentials for local development
- [**Dev/Prod Connectors**](/developers/build/connectors/templating) - Separate your production and development data sources
### Modeling and Last Mile ETL
- [**Models Overview**](/developers/build/models) - Overview of models available in Rill
- [**Getting Started with Models**](/developers/build/models/models-101) - What are models and important topics to remember as you create your own.
- [**Differences between SQL and YAML Models**](/developers/build/models/model-differences) - Compare SQL and YAML models and learn when to use
- [**SQL Models**](/developers/build/models/sql-models) - Transform and prepare your data
- [**Data Refresh**](/developers/build/models/data-refresh) - Schedule your data refresh
- [**YAML Models**](/developers/build/models/model-differences#yaml-models) - Incremental ingestion, data partitions, and more
- [**Additional Model Templating**](/developers/build/models/templating) - Separate your production and development settings
- [**Performance Optimization**](/developers/build/models/performance) - Optimize model performance and data refresh strategies
### Define Measures and Dimensions
- [**What are Metrics Views?**](/developers/build/metrics-view/what-are-metrics-views) - Learn about Metrics Views!
- [**Underlying Model/Table**](/developers/build/metrics-view/underlying-model) - Powering your metrics view with a `model` or `table`
- [**Time Series**](/developers/build/metrics-view/time-series) - The backbone of Rill Explore Dashboards, time
- [**Dimensions**](/developers/build/metrics-view/dimensions) - Expand your dimensions capabilities further in your metrics view
- [**Measures**](/developers/build/metrics-view/measures) - Aggregate your measures further in your metrics view
- [**Define Data Access**](/developers/build/metrics-view/security) - Define access and row access policies
### Build Visualizations
- [**Dashboards Types**](/developers/build/dashboards/dashboards-101) - What are the visualization options in Rill, and how are they different?
- [**Slice-and-Dice Dashboards**](/developers/build/dashboards/explore) - Explore and find insights in your data
- [**Traditional Visualizations**](/developers/build/dashboards/canvas) - Visualize your data with various chart types
- [**Canvas Components**](/developers/build/dashboards/canvas-widgets) - See all of our supported components!
- [**Define Dashboard Access**](/developers/build/dashboards/customization#define-dashboard-access) - Set a SQL boolean query that defines access to the dashboard
### Build Integrations with Custom APIs
- [**Custom APIs Overview**](/developers/build/custom-apis) - Create HTTP API endpoints to expose your Rill data
- [**SQL APIs**](/developers/build/custom-apis/sql) - Query models, tables, and external databases
- [**Metrics SQL APIs**](/developers/build/custom-apis/metrics-sql) - Query metrics views with automatic security inheritance
- [**Dynamic Queries & Templating**](/developers/build/custom-apis/templating) - Add parameters, user attributes, and conditional logic
- [**Security & Access Control**](/developers/build/custom-apis/security) - Multi-tenant APIs with custom attributes on tokens
### Project Settings
- [**Project Configuration**](/developers/build/project-configuration) - Configure your Rill Project and set default behavior
- [**Structure your Project**](/developers/build/structure) - Structure folder architecture in Rill
- [**Use your favorite IDE**](/developers/build/ide) - Utilize your favorite IDE to build Rill projects
- [**Debugging Rill Developer**](/developers/build/debugging/trace-viewer) - Troubleshoot dashboard access, trace your reconciled resources, and understand project logs
---
##### "Project Configuration"
---
title: "Project Configuration"
description: "Complete guide to configuring your Rill project with rill.yaml and other project files"
sidebar_label: "Project Configuration"
sidebar_position: 50
---
# Project Configuration
This guide covers all aspects of configuring your Rill project, from basic settings to advanced security and testing configurations.
## OLAP Connector
When you add an OLAP connector to your project, Rill automatically updates the `olap_connector` field in `rill.yaml` with the new connector name (e.g., `clickhouse`, `druid`).
If you create multiple connectors of the same type, Rill will number them sequentially. For example, if you create three ClickHouse connectors, they will be named:
- `clickhouse` (first connector)
- `clickhouse_2` (second connector)
- `clickhouse_3` (third connector)
The most recently created connector becomes the default and is referenced in `rill.yaml`.
```yaml
olap_connector: clickhouse
```
The default OLAP connector is used as the default `output` for all of your models unless otherwise specified.
## Model Defaults
Configure default behaviors for all models in your project. These settings control refresh schedules and materialization behavior that apply across all models unless overridden in individual model files.
### Model Refresh Schedule
Set up your project's model refresh schedule. You can override this in the model's YAML file if needed.
```yaml
models:
refresh:
cron: '0 * * * *'
```
For more information on configuring model refreshes, see the [models documentation](/developers/build/models) and [model refresh reference](/reference/project-files/models#refresh).
### Model Materialization
By default, models in Rill are created as views rather than materialized tables. While views enable a dynamic keystroke-by-keystroke modeling experience, materializing models as tables can significantly improve performance for complex queries and large datasets.
You can set a default materialization behavior for all models in your project:
```yaml
models:
materialize: true
```
This is particularly useful in production environments where performance is prioritized over the interactive modeling experience. Individual models can still override this setting using `-- @materialize: false` in their SQL file, or `materialize: false` in the YAML file.
:::tip When to materialize
- **Recommended:** Final models that power dashboards should be materialized for better performance
- **Consider:** Complex intermediate models with large datasets
- **Avoid:** Models with cross joins or when you need real-time keystroke-by-keystroke updates
For more details, see our [model performance guide](/developers/build/models/performance#materialization).
:::
## Metrics Views Defaults
Configure default settings for all metrics views in your project. These settings control time-related behavior and security policies that apply across all metrics views unless overridden in individual metrics view files. For comprehensive information on metrics views, see the [metrics view documentation](/developers/build/metrics-view).
### Metrics Views Time Modifiers
Set default time modifiers for all metrics views, such as `first_day_of_week` or `smallest_time_grain` as shown below. For more parameters, see the [metrics view reference page](/reference/project-files/metrics-views).
```yaml
metrics_views:
first_day_of_week: 1
smallest_time_grain: month
```
### Metrics Views Security Policy
By default, Rill is open to access (to your organization users), unless otherwise defined. To add project-level access to the Rill project, you can add a default metrics view security policy in the `rill.yaml` file. Like in a metrics view file, you can define the security as shown below. For more information, read our [data access documentation](/developers/build/metrics-view/security#examples).
```yaml
metrics_views:
security:
access: '{{ has "partners" .user.groups }}'
row_filter: "domain = '{{ .user.domain }}'"
```
:::tip Security Policy Rules
Rill YAML settings < (Metrics View YAML AND Dashboard YAML)
For detailed guide on security policies, review our [data access policies](/developers/build/metrics-view/security) doc.
:::
## Dashboard Defaults
Rill supports two types of dashboards: **Explores** (metrics-focused dashboards) and **Canvases** (custom visualization dashboards). You can set default configurations for each type. Learn more about creating and customizing dashboards in our [dashboard documentation](/developers/build/dashboards).
### Dashboard Security Policy
Similar to metrics views, you can set [security for a dashboard](/developers/build/dashboards/customization#define-dashboard-access). (Note that only `access` can be set at the dashboard level.)
```yaml
explores:
security:
access: "'{{ .user.domain }}' == 'example.com'"
canvases:
security:
access: '{{ has "dev" .user.groups }}'
```
### Explore Defaults
You are also able to set the `defaults` parameter in the explore dashboard to define your default time range, as well as the available `time_zones` and `time_ranges` in an Explore dashboard.
:::note Time Range Format
Time ranges use [ISO 8601 duration format](https://en.wikipedia.org/wiki/ISO_8601#Durations). Common examples:
- `PT24H` = 24 hours
- `P7D` = 7 days
- `P3M` = 3 months
- `P24M` = 24 months
:::
```yaml
explores:
defaults:
time_range: P3M
time_zones:
- UTC
time_ranges:
- PT24H
- P7D
- P14D
- P3M
```
### Canvas Defaults
Similarly, you can configure defaults for canvas dashboards:
```yaml
canvases:
defaults:
time_range: P7D
time_zones:
- UTC
time_ranges:
- PT24H
- P7D
- P14D
- P3M
```
:::tip Understanding Dashboard Default Behavior
When setting dashboard defaults, keep in mind that Rill Cloud saves each user's last state on dashboards. This means users will see the view they last used rather than the defaults configured above.
**Settings precedence (lowest to highest):**
Rill YAML settings < Dashboard YAML < Bookmarks in Rill Cloud < User Last State
Defaults are most useful for new users accessing a dashboard for the first time or when viewing in Rill Developer.
:::
## Environment Configuration
### Differentiating Dev and Prod Environments
Rill comes with default `dev` and `prod` properties defined, corresponding to Rill Developer and Rill Cloud, unless otherwise specified in the `rill start --environment (dev/prod)` command for Rill Developer. You can use these keys to set environment-specific YAML overrides or SQL logic.
For example, the following `rill.yaml` file explicitly sets the default materialization setting for models to `false` in development and `true` in production:
```yaml
dev:
models:
materialize: false
prod:
models:
materialize: true
```
:::note Specifying a custom environment
When using Rill Developer, instead of defaulting to `dev`, you can run your project in production mode using the following command:
```bash
rill start --environment prod
```
:::
## Variable Management
Variables in Rill enable dynamic templating throughout your project files. They can be used in SQL queries, YAML configurations, and security policies to make your project more flexible and maintainable.
### Setting Variables
Variables are defined in your `rill.yaml` file using the `env` key. This allows you to set variables that will be available in your Rill Cloud deployments while maintaining the ability to use different values locally during development.
```yaml
env:
numeric_var: 10
string_var: "string_value"
api_endpoint: "https://api.example.com"
```
Once defined, these variables can be referenced in your project files using template syntax: `{{ .env.variable_name }}`
**Example usage in a model:**
```sql
SELECT *
FROM users
WHERE status = '{{ .env.string_var }}'
AND score > {{ .env.numeric_var }}
```
## AI Configuration
You can provide context and instructions for AI tools (like [AI Chat](/guide/ai/ai-chat) and [MCP](/guide/ai/mcp)) using the `ai_instructions` field. This helps the AI agent deliver more relevant and actionable insights tailored to your specific needs.
For detailed instructions and examples on how to configure AI instructions at both the project and metrics view levels, please refer to our [AI Configuration guide](/developers/build/ai-configuration).
## Testing Security
### Test Access Policies in Rill Developer
Testing access policies in your local environment is a crucial step before deploying to Rill Cloud. This is done via the `mock_users` in the project file. You can create pseudo-users with specific domains, admin/non-admin roles, or user groups to ensure that access policies work as intended. For comprehensive information on security policies, see our [data access policies documentation](/developers/build/metrics-view/security).
Let's assume that the following security policy is applied to the metrics view.
```yaml
security:
access: "{{ .user.admin }} OR '{{ .user.domain }}' == 'rilldata.com'"
row_filter: "region = '{{ .user.region }}'"
```
In order to test both access to the dashboard, as well as the row filter, you can create the following in the project YAML.
```yaml
mock_users:
- email: royendo@rilldata.com
admin: true
region: us-west
- email: your_email@domain.com
groups:
- tutorial-admin
region: us-east
- email: your_email2@another_domain.com
region: europe
```
See our embedded example, [here](https://rill-embedding-example.netlify.app/security/filter-by-user).
### Custom Attributes
Embedded dashboards allow passing custom attributes (variables) from your application to control access and filtering. These attributes are set when generating the embed JWT token in your application code. For more information on embedding dashboards, see our [embedding documentation](/developers/embed/iframe).
To test embedded dashboards locally with custom attributes, add them to `mock_users`:
```yaml
- email: embed@rilldata.com
name: embed
custom_variable_1: Value_1 #this is passed at embed creation
custom_variable_2: Value_2 #this is passed at embed creation
```
See our [Custom Attributes Embedded Dashboard](https://rill-embedding-example.netlify.app/security/filter-by-custom-attributes) live!
Let's assume a similar setup to the above example. Within the metrics view, we define:
```yaml
security:
access: true
row_filter: >
app_site_name = '{{ .user.app_site_name }}' AND
pub_name = '{{ .user.pub_name }}'
```
Then within the application we are passing
```yaml
app_site_name='Sling'
pub_name='MobilityWare'
```
You can create a test mock user to ensure that this dashboard is working as designed with the following:
```yaml
- email: embed@rilldata.com
name: embed
app_site_name: 'Sling'
pub_name: 'MobilityWare'
```

## Feature Flags
:::warning Experimental Features
Feature flags enable experimental functionality that may be unstable or change before general availability. Use with caution in production environments.
:::
If you are interested in testing our upcoming features and experimental functionality, you can enable feature flags in your `rill.yaml` file. These flags allow you to access beta features and provide early feedback on new capabilities before they become generally available.
To enable feature flags, add the `features` section to your `rill.yaml`:
```yaml
features:
- cloudDataViewer
```
**Available feature flags:**
- `cloudDataViewer`: Enables the cloud data viewer interface for exploring data directly in the browser (default: `false`)
- `dimensionSearch`: Enables advanced dimension search functionality (default: `false`)
- `twoTieredNavigation`: Enables two-tiered navigation interface (default: `false`)
- `rillTime`: Enables Rill-specific time functionality (default: `false`)
- `hidePublicUrl`: Hides public URL sharing options (default: `false`)
- `exportHeader`: Enables export header functionality (default: `false`)
- `alerts`: Enables alerting features (default: `true`)
- `reports`: Enables reporting functionality (default: `true`)
- `chat`: Enables chat functionality (default: `true`)
- `dashboardChat`: Enables chat features within dashboards (default: `false`)
**How to enable**: Add the `features` section to your `rill.yaml`
**Reporting issues**: If you encounter issues with feature flags, please [contact support](/contact) with details about the flag and behavior.
For a complete list of available feature flags and their current status, see our [feature flags reference](https://github.com/rilldata/rill/blob/main/web-common/src/features/feature-flags.ts#L36) in the codebase.
## Complete Example
Here is an example YAML that uses many of our features.
```yaml
compiler: rillv1
display_name: Rill Project Dev
# The project's default OLAP connector.
# Learn more: https://docs.rilldata.com/reference/olap-engines
olap_connector: duckdb
# Project variables for templating
env:
numeric_var: 10
string_var: "string_value"
# Project Defaults
models:
refresh:
cron: '0 * * * *'
run_in_dev: true
metrics_views:
first_day_of_week: 1
smallest_time_grain: month
explores:
defaults:
time_range: P24M
time_zones:
- UTC
- America/Los_Angeles
- America/New_York
- Europe/London
- Europe/Paris
- Asia/Tokyo
- Australia/Sydney
time_ranges:
- PT24H
- P6M
- P12M
canvases:
defaults:
time_range: P24M
time_zones:
- UTC
- America/Los_Angeles
- America/New_York
- Europe/London
- Europe/Paris
- Asia/Tokyo
time_ranges:
- PT24H
- P7D
- P14D
- P30D
- P3M
- P6M
- P12M
# These are example mock users to test your security policies.
# Learn more: https://docs.rilldata.com/developers/build/metrics-view/security
mock_users:
- email: john@yourcompany.com
- email: jane@partnercompany.com
- email: your_email@domain.com
groups:
- tutorial-admin
- email: embed@rilldata.com
name: embed
custom_variable_1: Value_1 #this is passed at embed creation
custom_variable_2: Value_2 #this is passed at embed creation
features:
- cloudDataViewer
```
---
### Deploy
---
#### Deploy Dashboard
---
###### Deploy Dashboards
---
title: Deploy Dashboards
sidebar_label: Deploy Dashboards
sidebar_position: 00
---
## Overview
Deploying dashboards from Rill Developer allows you to share dashboards with other users, leverage [Rill Cloud capabilities](/guide/dashboards/explore), [embed Rill](/developers/embed/iframe) into other applications, and more!
The flow diagram below shows two options for deploying an existing project.
**Deploy via the UI or CLI using `rill project deploy`**:
```mermaid
graph LR;
A(Local code files);
B(Rill Cloud);
A--deploy (once)/update via UI -->B;
```
---
**Deploy via the CLI via `rill project connect-github`**:
```mermaid
graph LR;
A(Local code files);
B(Rill Cloud);
C(GitHub);
A--1. deploy via CLI (once) -->B;
C-- Continuous Deployment-->B;
A--2. Push changes to GitHub-->C;
```
## Deploying a project from Rill Developer
Starting from **v0.48**, we have introduced the possibility to push dashboards _directly from Rill Developer to Rill Cloud_. On the dashboard page, you can select the `Deploy` button and follow the steps to deploy to Rill Cloud.

Now that your project has been deployed to Rill Cloud, you will need to ensure that your users have access! Please refer to the [user management](/guide/administration/users-and-access/user-management) section.
If you make changes locally on Rill Developer, you will need to push the contents to Rill Cloud by selecting the `Update` button.

:::tip On an older version of Rill?
You can easily check the version of Rill that you are using in Rill Developer by running the following command:
```bash
rill --version
```
If you are on an older version of Rill, it is **strongly recommended** to [upgrade](/developers/get-started/install#upgrade-to-the-newest-version-of-rill-developer) to the latest version.
:::
### Syncing your GitHub Repository
:::note GitHub app permissions
This assumes that the installed GitHub app in your organization has write access. If unsure, please check with your GitHub admin.
The required permissions are:
- Read access to metadata and pull requests
- Read and write access to administration and code
:::
At this point, you have the option to connect your Rill project to a GitHub Repository.
Navigating to the Settings page and selecting `Connect to GitHub` will prompt you to login and create a repository for your project. If you've already created a repository, check the box 'I've created a GitHub Repo' and add the permissions for Rill to access the repository.
:::info Check with your GitHub organization admin
If you're not the admin of your GitHub organization, they will likely need to first install the Rill Cloud app in your organization before you can proceed with deploying a project. After the Rill Cloud app is installed, it should have the following privileges:
:::

Once the permissions to the repository have been confirmed and set, you can continue to select the repository in the dropdown.

Once completed, you'll see the newly updated repository on the UI of your settings page!

:::warning Still unable to connect?
If you encounter issues, check that the app installation is not pending. Go to your organization's settings and click on Installed GitHub Apps. You will see a section of Pending GitHub Apps installation requests. If you're an Owner or App Manager, grant access to the Rill app if it is pending."
:::
## Deploying a project via the CLI
:::note
Starting from v0.49, we have deprecated `rill deploy` in favor of `rill project deploy` and `rill project connect-github`. For more information on the `rill deploy` command click [here](#deprecated-rill-deploy).
:::
### Deploy project without GitHub Repository
You can add a GitHub Repository later.
```
rill project deploy
Using org "Rill_Learn".
Starting upload.
All files uploaded successfully.
Created project "Rill_Learn/my-rill-tutorial". Use `rill project rename` to change name if required.
...
Your project can be accessed at: https://ui.rilldata.com/Rill_Learn/my-rill-tutorial
Opening project in browser...
```
If you have not already [configured your connections' credentials](https://docs.rilldata.com/developers/build/connectors/credentials), you will be reminded here which connections are required.
**First deployment**
If this is your first deployment to Rill Cloud, you will get prompted to either sign up or log in (if you have an existing account on [Rill Cloud](https://ui.rilldata.com/)). Proceed with the sign up and email verification process for new users or authorization process for existing users. As a new user, you can expect to see the following page:

**Project Uploaded Successfully**
Once the project has been uploaded to Rill Cloud, you should be able to see the following page:

### Deploy Project with Repository
Follow the instructions in the Terminal to login to GitHub (if not already done so), and select your repository.
If you do not set any parameters, Rill will infer the project name based on the folder path and use this as both the repository and project name. If there are any overlaps, we will request for a new name.
```bash
rill project connect-github
No git remote was found.
? Do you want to create a repo? Yes
? Select a GitHub account for the new repository royendo
Repository name "my-rill-tutorial" is already taken
? Please provide alternate name my-rill-tutorial-cli
Request submitted for creating repository. Checking completion status
Successfully created repository on "https://github.com/royendo/my-rill-tutorial-cli"
Pushing local project to GitHub
Successfully pushed your local project to GitHub
Using org "Rill_Learn".
Created project "Rill_Learn/my-rill-tutorial-cli". Use `rill project rename` to change name if required.
Rill projects deploy continuously when you push changes to GitHub.
...
Your project can be accessed at: https://ui.rilldata.com/Rill_Learn/my-rill-tutorial-cli
Opening project in browser...
```
Once completed, you will see the following in the settings page. Note that the GitHub repository is already set up!

## Continuous Deployment
Whether you decide to manage your Rill projects using GitHub or by re-running `rill project deploy`, Rill should automatically detect changes that you have pushed locally and update your deployed project accordingly. Depending on the changes, this may result in a project reconciliation. If you are experiencing issues with the project after pushing changes with the CLI, please refer to the project's status page for more information, or run the following command:
```
rill project status
```
Likewise, if using the UI by selecting the `Update` button, Rill will detect the changes in files and update your deployed project accordingly. Along with the above CLI command, you can view the status of the objects in the Status page.
:::tip Interested in using GitLab?
Check out our documentation on deploying a [Rill project using GitLab](/developers/deploy/deploy-dashboard/deploy-from-cli)!
:::
## Change your production branch
By default, Rill deploys from the [default branch](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches#about-the-default-branch) of your Git repository. You can change this to any branch you want.
To deploy your project from a different branch, run the following command:
```bash
rill connect-github --prod-branch [PROD-BRANCH]
```
## Deploy from a monorepo
If your Rill project is in a sub-directory of a Git repository, use the `--subpath` option when creating your project:
```
rill connect-github --subpath path/to/rill/project
```
:::warning
Note that you must run `rill connect-github` from the root of your Git repository, **not** the root of your Rill project.
:::
## Deprecated Rill Deploy
When running `rill deploy` you have two options:
1. Enable automatic deploys to Rill Cloud via GitHub
2. Disable automatic deploys to Rill Cloud via GitHub
```
rill deploy
? Enable automatic deploys to Rill Cloud from GitHub?
```
### Enable Automatic deploys
Like running `rill project connect-github`, you will be [prompted to create a github repository](#deploy-project-with-repository). Once created, Rill will deploy the project. You can confirm that the project has the correct repository linked from the UI on the settings page.
### Disable Automatic deploys
In this case, the project will be deployed to Rill Cloud without a GitHub repository connected. You can always [add a repository via the UI](#syncing-your-github-repository) at a later time.
---
###### Deploy to Rill Cloud from GitLab
---
title: Deploy to Rill Cloud from GitLab
description: How to set up continuous deploys to Rill Cloud from GitLab
sidebar_label: Deploy from GitLab
sidebar_position: 10
---
While Rill Cloud natively integrates with [GitHub](https://github.com), you can also deploy your Rill project from [GitLab](https://about.gitlab.com/) using direct uploads from a [GitLab CI/CD pipeline](https://docs.gitlab.com/ee/ci/quick_start/).
Follow these steps to set up continuous deployment from GitLab to Rill Cloud:
1. Create a new GitLab repository and push your Rill project to it.
2. On your local, [authenticate with Rill Cloud](/guide/administration/users-and-access/user-management#logging-into-rill-cloud) and create an organization (replace `my-org-name` with your desired name):
```bash
rill login
rill org create my-org-name
```
3. Create the project in Rill Cloud
```bash
rill project deploy
```
:::note Multiple branches
If your repo contains multiple branches ensure the branch you want to deploy from via
```bash
rill project edit --project my-project-name --prod-branch my-branch-name
```
:::
4. Provision a Rill Cloud [service account](/reference/cli/service/create) called `gitlab-ci` and copy its access token:
```
rill service create gitlab-ci
```
5. Set the service token as a CI/CD variable called `RILL_SERVICE_TOKEN` in GitLab (from the repository page, it's under _Settings > CI/CD > Variables_).
6. Create a file named `.gitlab-ci.yml` at the root of the repository containing your Rill project. Paste the following contents into it (replace `my-org-name` and `my-project-name` with your desired names):
```yaml
deploy-rill-cloud:
stage: deploy
script:
- curl -L -o $HOME/rill.zip https://cdn.rilldata.com/rill/latest/rill_linux_amd64.zip
- unzip -d $HOME $HOME/rill.zip
- git checkout -B "$CI_COMMIT_REF_NAME" "$CI_COMMIT_SHA"
- $HOME/rill project deploy --org my-org-name --project my-project-name --interactive=false --api-token $RILL_SERVICE_TOKEN
```
Your Rill project should now automatically deploy to `ui.rilldata.com/my-org-name/my-project-name` each time changes are pushed to GitLab!
:::note File size limits
We enforce a file size limit of 100mb so ensure you do not unpack the rill binary in the repo root or add it to your .gitignore
:::
---
###### "GitHub Basics"
---
title: "GitHub Basics"
description: GitHub Basics for Deployment
sidebar_label: "GitHub Basics"
sidebar_position: 20
---
## Overview
:::info New to Git?
This page makes the deploy process easier for those who are less familiar with GitHub by walking you through the steps to use the UI rather than the GitHub command line interface. Much simpler for those who prefer a graphic interface!
:::
To share dashboards with other users, Rill utilizes GitHub as a means of version control - effectively creating BI-as-code. There are several advantages to Git as the backend for dashboards: develop locally, integrate into existing development workflows, and manage versions + change control. While many users are familiar with Git and basic commands, there are other Rill users who are relatively new to Git.
In this section, we will outline:
- Installing the GitHub Desktop app (recommended for those less familiar with Git versus using the command line)
- Setting up your repository (repo) and cloning to your local
- Pushing changes to Git
- Deploying Rill to your new org to share dashboards, set up alerts and receive scheduled reports

## Installing Git
-To download the GitHub app for a UI driven workflow, visit the [install site here](https://docs.github.com/en/desktop/installing-and-authenticating-to-github-desktop/installing-github-desktop).
- In your computer's Downloads folder, double-click the GitHub Desktop zip file.
- After the file has been unzipped, double-click the GitHub Desktop application file.
- GitHub Desktop will launch after installation is complete.
:::note Creating a GitHub account
If you do not already have a GitHub account, you'll need to create one as part of this process. For those looking to deploy to a company repo, we would suggest requesting access to a specific repo where you can get started.
:::
## Cloning Locally
Once you have downloaded GitHub and connected your account, you'll be able to create a new repository. Think of the repository as a shared cloud drive for the configuration files (sources, models, dashboards) that power Rill.
- First, go to GitHub.com and create a new repository - clicking the ```New``` button on the homepage\

- After you name your repository and click save, you'll see the option to ```Set up in Desktop```. This will open the GitHub desktop app directly to "clone" the repo to your local computer.
-

- Select the folder on your computer where you'd like to build with Rill. Confirm the location you cloned for the next step.

- Open the Terminal and go to your local copy of the repo (e.g. the above would be ```cd Documents/GitHub/rill-poc/testrepo```)
- Run the Rill install command ```curl https://rill.sh | sh``` (or run ```rill start``` if you've already installed)
Now - any changes you make locally can be synced directly with your GitHub repo in one click! You can revisit our [build section](/developers/build/connectors) for refreshers on connecting to new sources, modeling data, or building dashboards.
## Pushing Changes
After installing Rill and adding sources, your local project will now contain a variety of new files - source definitions, SQL models for transformation, and dashboards for analysis. GitHub automatically recognizes those files as new and you can now push those changes to your cloud repository.
First, open the GitHub app and select your repo. You should now see a variety of new changes (that can then be committed):

Now, in order to actually commit these changes to your repo, you can take the following steps:
- On the bottom left, you can describe the changes (or add something like "initial commit")
- Select the now blue _Commit to main_ button on the bottom left
- Click _Publish Branch_ on the top right - and that's it. Any future changes would follow the same two steps - add a comment, commit and publish!
:::warning Avoid Syncing Data Files
During the commit process, make sure to **uncheck** files you do not wish to sync to your cloud repo (or add the appropriate exclusions in your `.gitignore` file). In particular, if you were working with local data files, those will now be in the repo including ```tmp``` files and/or sources (e.g. parquet or CSV files). GitHub has a limit on cloud storage so instead consider adding that data to a [cloud object storage location](/developers/build/connectors) (S3, GCS, Azure).
:::
:::tip Make small changes directly in github
For any larger changes, we would strongly suggest developing locally to see the effects of that change. However, for smaller changes (add a simple metric, changing the name of a dimension, etc.), some users _may_ prefer to edit directly inline on [GitHub](https://github.com) directly.
:::
## Deploying Rill
Now that your dashboards should be fully synced to GitHub, version controlled, and available to be edited by others. To make the dashboard fully cloud-enabled, you can return to your terminal and run ```rill project connect-github```. With everything synced, you should now be able to create a [new organization](/guide/administration/organization-settings#organization) within Rill Cloud and push your dashboard to the cloud for shared collaboration. Any future changes should automatically be present in your deployed dashboards on [Rill Cloud](https://ui.rilldata.com) once committed and new dashboards will appear automatically as well (no deploy command needed). More details on [deployment here](/developers/deploy/deploy-dashboard).
:::info Have further questions? We'd love to hear from you!
We know that utilizing Git may be new to some - please don't hesitate to [reach out](/contact) to us with any questions and we'd be happy to assist.
:::
---
##### Rill Cloud vs Rill Developer
---
title: Rill Cloud vs Rill Developer
sidebar_label: Rill Cloud vs Rill Developer
sidebar_position: 10
hide_table_of_contents: false
---
## What is Rill Cloud and Rill Developer?
Rill offers two unique but complementary experiences within our broader product suite, **Rill Cloud** and **Rill Developer**.
As the name suggests, Rill _Developer_ is designed with the developer in mind, where project development actually occurs. Rill Developer is meant for the primary developers of project assets and dashboards, allowing them to import, wrangle, iterate on, and explore the data before presenting it for broader consumption by the team. Rill Developer is meant to run on your local machine - see here for some [recommendations and best practices](/developers/tutorials/performance#local-development--rill-developer) - but it is a simple process to [deploy a project](/developers/deploy/deploy-dashboard) once ready to Rill Cloud.
Rill Cloud, on the other hand, is designed for our dashboard consumers and allows broader team members to easily collaborate. Once the developer has deployed the dashboard onto Rill Cloud, these users will be able to utilize the dashboards to interact with their data, set alerts / bookmarks, investigate nuances / anomalies, or otherwise perform everyday tasks for their business needs at Rill speed.
## Is Rill Cloud a higher offering than Rill Developer?
Based on the naming, it might be confusing and easy to assume that Rill Cloud is our "higher" offering but **that is not the case!** Similarly, Rill Developer is _not meant to be used as a standalone tool either_.
Rill Developer and Rill Cloud are to be used together. Rill Developer provides a space for our developers to define and test any new or needed changes to the data and/or dashboards before pushing to our Rill Cloud users, who need stable access to working dashboards. Then, once finalized, these dashboards are deployed to the Rill Cloud project for broader consumption and to power business use cases.
:::info Isn't Rill Developer enough?
Please note that a common **misnomer** is that Rill Developer can be a sufficient replacement for Rill Cloud. They both serve different purposes but are meant to be used _in conjunction_. Rill enables speed of exploration and is easy to use for developers, allowing the project to be iterated on quickly. Rill Cloud then allows for shared collaboration at scale, especially for production deployments.
:::
### Why deploy to Rill Cloud?
Rill Developer is an extremely strong tool for deep-diving into your data, as it allows users to import sources from many destinations and join these tables together to create something useful in a slice-and-dice visualization. Many times the feedback we receive is, "`I didn't even know my data had an issue in it,`" or, "`In a few minutes, I was able to make new insights into my data that would've taken me hours.`" This is great, but if that's possible in Rill Developer, why publish the dashboard to Rill Cloud?
## Rill Developer
Rill Developer is designed around developers. Using a familiar IDE-like interface, developers are able to import data, create SQL models, and create metrics views. Many of the underlying files in Rill Developer are either SQL or YAML files. Once data is imported into Rill (and the underlying OLAP engine), developers are able to perform last-mile ETL changes using one or a series of SQL models (as their own [DAG](https://en.wikipedia.org/wiki/Directed_acyclic_graph#:~:text=A%20directed%20acyclic%20graph%20is,a%20path%20with%20zero%20edges)). You can then create and materialize your ["One Big Table"](/developers/build/models/models-101#one-big-table-and-dashboarding) for your dashboard needs. Finally, any specifications for your dimensions and measures can be defined and tested in Developer's dashboard preview.

## Rill Cloud
Once the dashboard has been [deployed to Rill Cloud](/developers/deploy/deploy-dashboard), the dashboard can be shared with others and viewed by other members of your Rill Cloud organization. As you can see below, the UI is different from Developer. Upon accessing Rill Cloud, a user will be able to view all the projects they have been granted access to by project admins.

After selecting a specific project, they will be directed to a list of dashboards. From Rill Cloud, the dashboard consumer does not have the ability to make any modifications to sources or models. However, they are given some additional capabilities that are not accessible in Rill Developer, such as alerting, creating bookmarks or shareable public URLs, checking the project status, and more.
:::info Dashboard 101
For more details about using a Rill Cloud dashboard, please refer to our [Explore section](/guide/dashboards/explore)!
:::
---
##### Deployment Credential Considerations
---
title: Deployment Credential Considerations
description: Configuring credentials for your deployed project on Rill Cloud
sidebar_label: Configure Deployment Credentials
sidebar_position: 10
---
:::tip Development and Production Setup
We recommend reviewing the [Development/Production Setup](/developers/build/connectors/templating) documentation before deploying your project to Rill Cloud to ensure that your local development and production environments have been separated.
This will ensure that your shared dashboard will be decoupled from your local Rill Developer environment, and you can further develop your dashboards locally without worrying about data availability.
:::
When deploying a project, credentials that have been defined in your `.env` file will be automatically passed into your Rill Cloud project. However, for [remote sources](/developers/build/connectors) that are dynamically retrieving your credentials via the CLI, such as S3 and GCS, you will need to ensure that these are [defined in the .env file]( /guide/administration/project-settings/variables-and-credentials#credentials-naming-schema).
[Local credentials](/developers/build/connectors/credentials#setting-credentials-for-rill-developer) are used by Rill Developer to connect to sources from your local machine, while deployment credentials are used by Rill Cloud for production workloads. There are a [few ways to set up credentials in Rill Developer](/developers/build/connectors/credentials/#setting-credentials-for-rill-developer); however, you will need to ensure that they are set up in your `.env` file for a seamless experience.
If you have defined your connector's credentials in your `.env` file, these will be deployed along with your project. You should see the credentials in [your project's settings page]( /guide/administration/project-settings/variables-and-credentials#modifying-variables-and-credentials-via-the-settings-page).

## Service Accounts
:::info Separating development and production credentials
As a general best practice, it is strongly recommended to use service accounts and dedicated service credentials for projects deployed to Rill Cloud, especially when used in a production capacity. This is covered in more detail in our [Development/Production Setup documentation](/developers/build/connectors/templating).
:::
Service accounts are non-human user accounts that provide an identity for processes or services running on a server to interact with external resources, such as databases, APIs, and cloud services. Unlike personal user accounts, service accounts are intended for use by software applications or automated tools and do not require interactive login. In the context of Rill, service accounts are credentials that should be used for projects deployed to Rill Cloud.
### Why are service accounts important?
Using service accounts for production workflows and pipelines is a general best practice for several reasons:
1. **Improved Security Posture**: Service accounts are specialized accounts used specifically by applications, as opposed to human users, to interact with data sources and other services. They help in implementing the principle of _least privilege_ by restricting permissions to only what is necessary for the application to function. This minimizes the potential damage when an account or set of credentials are compromised.
2. **Auditing and Monitoring**: Using service accounts makes it easier to audit and monitor access and activities. Since these accounts are used exclusively by applications (in this case Rill), any data access or actions performed can be traced back to the specific application, simplifying the process of identifying unusual or unauthorized activities.
3. **Credential Management**: Service accounts facilitate better management of credentials. For instance, when humans manage and share credentials, there's a higher risk of credentials being leaked or mishandled. Service accounts can be managed programmatically, reducing human error and improving security through automated rotations and stricter access controls.
4. **Scalability and Automation**: As organizations grow and deploy more applications, the use of service accounts allows for scalable and automated access management. It's easier to programmatically control access for multiple service accounts across different environments and services, fitting well into infrastructure as code (IaC) practices. Similarly, this ensures deployed projects (on Rill Cloud) don't share credentials with other applications.
5. **Compliance and Governance**: For regulatory compliance, using service accounts helps enforce data governance policies by ensuring that access is granted _according to the defined roles and responsibilities of the application_. It also aids in ensuring that data handling complies with policies and regulations since access patterns and permissions are clearly defined and can be audited.
6. **Reliability**: Applications using service accounts are less likely to experience downtime due to credential changes. Unlike user accounts, which may have passwords that expire or change frequently, service accounts can be configured with long-lived credentials or managed identity solutions that automatically handle authentication, reducing the risk of disruptions. Similarly, service accounts are not tied to the lifecycle of employee accounts, which help ensure that automated processes aren't potentially disrupted by personnel changes (such as offboarding an employee).
### Service Account Best Practices
- _Secure Storage of Credentials_: Store service account credentials securely, using encrypted storage solutions and access controls to prevent unauthorized access.
- _Regular Rotation of Credentials_: Regularly update service account passwords and keys to reduce the risk of compromise.
- _Minimum Necessary Permissions_: Grant **only** the permissions necessary for the specific tasks the service account needs to perform, and review permissions regularly to adapt to changes in application functionality.
- _Monitoring and Logging_: Depending on your organization's security needs, you can also consider implementing monitoring and logging of all access and actions taken by service accounts to detect and respond to anomalous activities promptly.
:::warning Be careful of overriding local credentials and/or pushing the wrong credentials to Rill Cloud
When using service accounts, it is very likely that different or even personal credentials are being used in local development (i.e., Rill Developer). Therefore, it is worth double-checking that the correct credentials are being used or set before [syncing credentials](/developers/build/connectors/credentials#pulling-credentials-and-variables-from-a-deployed-project-on-rill-cloud) between your local instance of [Rill Developer and Rill Cloud](/developers/deploy/cloud-vs-developer) using the `rill env push` and `rill env pull` commands respectively.
:::
---
##### Publish your Dashboards to Rill Cloud
---
title: Publish your Dashboards to Rill Cloud
sidebar_label: Publish
sidebar_position: 0
---
Rill Developer is a great tool for building, testing, and viewing your data locally, but once you're ready to share your findings, you'll need to publish the dashboard to Rill Cloud!
### Concepts
- [**Rill Cloud vs Rill Developer**](/developers/deploy/cloud-vs-developer) - Before you deploy, understand the difference between Rill Cloud and Rill Developer
### Credentials
- [**Deployment Credentials**](/developers/deploy/deploy-credentials) - Confirm deployment credentials based on your deployment environment
### Deployment
- [**Deploy your Dashboard**](/developers/deploy/deploy-dashboard) - Publish your dashboard to Rill Cloud
### Post-Deployment Steps
- [**Managing Project Errors**](/developers/deploy/project-errors) - Troubleshooting errors in Rill Cloud
---
##### Managing Project Errors
---
title: Managing Project Errors
description: Configure alerts and manage errors for deployed projects in Rill Cloud
sidebar_label: Managing Project Errors
sidebar_position: 25
---
When you deploy to Rill Cloud, projects can encounter errors—from missing credentials to data type mismatches. This guide focuses on managing errors in deployed projects and setting up automated alerts.
:::info General Troubleshooting
For general troubleshooting guidance, error message explanations, and debugging techniques, see the [Debugging Rill Projects](/developers/build/debugging) documentation.
:::
## How Rill Handles Errors
Rill's error management approach ensures visibility and isolation:
- **Visibility:** View project status at the resource level via the `Status` tab or [`rill project status`](/reference/cli/project/status) CLI command
- **Isolation:** Errors are contained to individual resource trees—if one dashboard fails, others remain available
- **Fallback:** Rill attempts to serve from the most recent valid state when possible
:::tip Check upstream dependencies
The surfaced error might not be the root cause. A dashboard error could stem from an underlying model timeout. Always check the [project status page](/guide/administration/project-settings#checking-deployment-status) to trace errors to their source.
:::
## Deployment-Specific Error Scenarios
Most errors will surface during local development in Rill Developer. However, after deploying to Rill Cloud, you may encounter additional issues:
1. **Production configuration missing** - Your YAML files reference `prod:` parameters that have been defined incorrectly. Verify your [dev/prod setup](/developers/build/connectors/templating).
2. **Timeouts, OOM** - Production data volumes may be larger than local development data, leading to timeouts and out-of-memory issues. [Contact us](/contact) if you see any related error messages.
To troubleshoot deployment errors:
1. **Check the resource status** in the [project status page](/guide/administration/project-settings#checking-deployment-status)
2. **Review project logs** using `rill project logs` or the Rill Cloud UI
3. **Compare with local behavior** - If it worked locally, check production-specific configuration differences
## Setting Up Error Alerts
You can configure alerts to automatically notify you when project errors occur. Once set up, you'll receive notifications (via email or Slack) whenever any resource in your project enters an error state.
Besides alerting on project errors, it is possible to configure generic alerts in your dashboards based on specific thresholds or conditions being met. For more details, check out our [alerts documentation](/guide/alerts)!
### Configure an email alert
To configure an email alert for project errors, add a file named `project_errors.yaml` to your Rill project with the contents below. Remember to update the `recipients` field to your desired alert recipients.
```yaml
type: alert
# Check the alert every 10 minutes.
refresh:
cron: "*/10 * * * *"
# Query for all resources with a reconcile error.
# The alert will trigger when the query result is not empty.
data:
resource_status:
where_error: true
# Send notifications by email
notify:
email:
recipients: [john@example.com]
```
This will give you a good idea of what object has an issue, and you can browse the [status page](/guide/administration/project-settings#checking-deployment-status) for more information.
After making these changes, you should commit and [push these changes](/developers/deploy/deploy-dashboard/github-101#pushing-changes) to your git repository.
### Configure a Slack alert
To configure a Slack alert for project errors, first follow the Slack configuration steps described on [Configuring Slack integration](/developers/build/connectors/services/slack#setting-up-the-slack-integration). Next, add a file named `project_errors.yaml` to your Rill project with the contents below. Remember to update the `channels` field to your desired destination channel.
```yaml
type: alert
# Check the alert every 10 minutes.
refresh:
cron: "*/10 * * * *"
# Query for all resources with a reconcile error.
# The alert will trigger when the query result is not empty.
data:
resource_status:
where_error: true
# Send notifications in Slack.
# Follow these steps to configure a Slack token: https://docs.rilldata.com/guide/alerts/alerts/slack.
notify:
slack:
channels: [rill-alerts]
```
After making these changes, you should commit and [push these changes](/developers/deploy/deploy-dashboard/github-101#pushing-changes) to your git repository or update your Rill project via the Deploy button.
---
### Embed
---
##### Embed Dashboards in an Iframe
---
id: iframe
title: Embed Dashboards in an Iframe
description: Embed Rill dashboards in your own applications using iframes
sidebar_label: Iframe
sidebar_position: 10
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
## Introduction
Rill Cloud provides the ability to embed dashboards as components in your own application using iframes, with a few different options:
- Embedding individual dashboards as standalone iframes
- Embedding individual dashboards with the ability to navigate to other dashboards (that exist in the _same_ project)
- Embedding the dashboard list page present in a Rill project (with the ability to select and navigate between dashboards)
When embedding Rill, you need to generate a service token for your backend to request an authenticated iframe URL via the Rill API. Afterwards, the iframe URL can be passed to your frontend application for rendering. Here's a high-level diagram of what this flow looks like:
```mermaid
sequenceDiagram
participant A as ui.ezcommerce.com
participant B as api.ezcommerce.com
participant C as api.rilldata.com
participant D as ui.rilldata.com/-/embed
participant E as node.region.runtime.rilldata.com
A ->> B: Get iframe URL
B ->> B: Resolve the user's email using ezcommerce's own auth
B ->>+ C: Get iframe URL for: project="ezcommerce" user="john@example.com" (uses Rill service token)
Note right of C: 1. Lookup deployment 2. Generate JWT 3. Build iframe URL
C ->>- B: iframe URL
B ->> A: iframe URL
A ->> D: Render iframe URL
loop
D ->> E: Request data (gets host and JWT from iframe URL query)
E ->> D: Data with policies applied for user="john@example.com"
end
```
## Create a service token
Use the Rill CLI to create a service token for your current organization using the following command:
```bash
# Create with organization role
rill service create --org-role admin
# Or create with project-specific role
rill service create --project --project-role admin
```
:::info
For comprehensive documentation on service tokens, including roles, custom attributes, and management, see [Service Tokens](/guide/administration/access-tokens/service-tokens). Also see the [CLI reference](/reference/cli/service) for command details.
:::
:::note Why is the admin role required?
Service tokens used for iframe embedding require the `admin` role to access the deployment management APIs that generate iframe URLs. Using the `viewer` role will result in a 403 permission error.
:::
:::caution
Service tokens can have broad permissions and should be handled confidentially. Therefore, the service token itself should **not** be integrated directly in frontend or other user-facing code that can be exposed publicly.
:::
## Backend: Build an iframe URL
You should implement an API on your backend that uses the service token to retrieve and return an iframe URL from Rill's API (which is hosted on `api.rilldata.com`).
There are multiple reasons why the iframe URL must be constructed on your backend:
- To avoid leaking your master Rill service token in the browser
- To allow you to use your own authentication and authorization logic to restrict access to the dashboard
- To optionally use your backend's context about the authenticated user to include user attributes in the iframe URL for enforcement of row-level security policies
Here are examples of how to get an iframe URL using different languages:
```bash
curl -X POST --location 'https://api.rilldata.com/v1/orgs//projects//iframe' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ' \
--data-raw '{
"type": "explore",
"resource": "",
"user_email": ""
}'
```
JavaScript (Node.js) with Express.js
```js
const express = require('express');
const fetch = require('node-fetch');
const rillServiceToken = "";
const rillOrg = "";
const rillProject = "";
const app = express();
app.use(express.json());
app.post('/api/rill/iframe', async (req, res) => {
const dashboardName = req.body.resource;
try {
const response = await fetch(`https://api.rilldata.com/v1/orgs/${rillOrg}/projects/${rillProject}/iframe`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${rillServiceToken}`,
},
body: JSON.stringify({
type: 'explore',
resource: dashboardName,
// Optionally include the end user's email address for security policies:
// user_email: '',
// Optionally set theme and theme mode:
// theme: 'my-custom-theme',
// theme_mode: 'dark', // Options: 'light', 'dark', 'system'
}),
});
const data = await response.json();
if (response.ok) {
res.json(data);
} else {
throw new Error(data.message);
}
} catch (error) {
res.status(500).json({ error: error.message });
}
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
```
Python with Flask
```python
from flask import Flask, request, jsonify
import requests
app = Flask(__name__)
@app.route('/api/rill/iframe', methods=['POST'])
def get_rill_iframe():
dashboard_name = request.json.get('resource')
try:
response = requests.post(
'https://api.rilldata.com/v1/orgs//projects//iframe',
headers={
'Content-Type': 'application/json',
'Authorization': 'Bearer ',
},
json={
'type': 'explore',
'resource': dashboard_name,
# Optionally include the end user's email address for security policies:
# 'user_email': '',
# Optionally set theme and theme mode:
# 'theme': 'my-custom-theme',
# 'theme_mode': 'dark', # Options: 'light', 'dark', 'system'
}
)
response.raise_for_status()
data = response.json()
return jsonify(data)
except requests.RequestException as e:
return jsonify(error=str(e)), 500
if __name__ == '__main__':
app.run(port=3000)
```
Go with net/http
```go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
func getRillIframe(w http.ResponseWriter, r *http.Request) {
var reqBody map[string]string
json.NewDecoder(r.Body).Decode(&reqBody)
dashboardName := reqBody["resource"]
requestBody, err := json.Marshal(map[string]string{
"type": "explore",
"resource": dashboardName,
// Optionally include the end user's email address for security policies:
// "user_email": "",
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp, err := http.Post("https://api.rilldata.com/v1/orgs//projects//iframe", "application/json", bytes.NewBuffer(requestBody))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var respBody map[string]any
err = json.Unmarshal(body, &respBody)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(respBody)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func main() {
http.HandleFunc("api/rill/iframe", getRillIframe)
fmt.Println("Server started at port 3000")
http.ListenAndServe(":3000", nil)
}
```
Java With Spring Boot
```java
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import java.util.HashMap;
import java.util.Map;
@RestController
public class DashboardController {
@PostMapping("/api/rill/iframe")
public ResponseEntity> getRillIframe(@RequestBody Map payload) {
String dashboardName = (String) payload.get("resource");
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
headers.set("Authorization", "Bearer ");
Map request = new HashMap<>();
request.put("type", "explore");
request.put("resource", dashboardName);
// Optionally include the end user's email address for security policies:
// request.put("user_email", "");
HttpEntity
The API accepts the following parameters:
| Parameter | Description | Required |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| resource | The name of the dashboard to embed | No (if not specified, `navigation` should be set to `true`) |
| type | The type of the dashboard identified by `resource` (options: `explore`, `canvas`) | No (defaults to `explore`) |
| navigation | Boolean whether to enable navigation and allow users to navigate to other dashboards (`false` will hard embed and allow access to a single dashboard; `true` allows navigation) | No (defaults to `false`) |
| theme | If [themes](/developers/build/dashboards/customization#changing-themes--colors) are being used, the specific theme to pass to the embedded dashboard | No (set to the name of the theme) |
| theme_mode | The theme mode to use for the embedded dashboard. Valid values: `light`, `dark`, `system`. When set to `system`, the dashboard will follow the user's system preference. | No (defaults to `light`) |
| user_email | Email of the end user accessing the embed. Used to populate attributes for use in security policies. | No (only one of `user_email` or `attributes` should be passed in) |
| attributes | Raw attributes about the end user. Used to populate attributes for use in security policies. | No (only one of `user_email` or `attributes` should be passed in) |
| external_user_id | A stable identifier for an external end user. Enables per-user state. | No |
| ttl_seconds | The time to live for the iframe URL | No (Default: 86400) |
The response of the POST request will then contain an `iframeSrc` value that can be used to embed the dashboard or set of dashboards in your application. It will also contain a `ttlSeconds` value, which indicates how long the iframe URL will be valid for. _After the TTL has elapsed_, the iframe URL needs to be refreshed as the underlying access token being used will no longer be valid (for security purposes). Here's an example response:
```json
{
"iframeSrc": "https://ui.rilldata.com/-/embed?access_token=&instance_id=&type=&resource=&runtime_host=",
"runtimeHost": "",
"instanceId": "",
"accessToken": "",
"ttlSeconds": 86400
}
```
## User identity in embeds
The `user_email`, `attributes` and `external_user_id` parameters serve two purposes in the embeds:
- **User attributes for security policies:** `user_email` and `attributes` determine which attributes (such as `email`, `domain`, and `admin`) are available for use in [security policies](/developers/build/metrics-view/security).
- **Per-user state:** `external_user_id` establishes a stable user identity that isolates per-user features such as AI chat history. Without a user identity, these features are not available.
Only one of `user_email` or `attributes` can be provided for a given iframe. The `external_user_id` parameter can optionally be combined with either of them. Here is how each parameter works:
- `user_email`: Looks up the user in Rill Cloud by email and populates their standard attributes. If no matching user is found, it generates limited attributes with only the fields `email`, `domain` (the part of the email after `@`) and `admin` (set to `false`). The derived `domain` is commonly used in security policies (e.g. `app_site_domain = '{{ .user.domain }}'`). Does not enable per-user state on its own; combine with `external_user_id` to enable per-user state.
- `attributes`: Passes the provided attributes through directly. Make sure to include all attributes referenced in your security policies (e.g. `email`, `domain`, `admin`, or custom attributes like `tenant_id`). Does not enable per-user state on its own; combine with `external_user_id` to enable per-user state.
- `external_user_id`: Any stable identifier for the end user. This is usually the user's ID in your own database. Setting it enables per-user state such as AI chat history.
## Embedding the project vs embedding an individual dashboard
One of the most common differences between how developers may wish to iframe Rill is whether they wish to embed at the project level or individual dashboard level. This behavior can be controlled through the combination of the `resource` and `navigation` properties!
If you wish to embed a single dashboard **only**, your payload might look like:
```json
{
"type": "explore",
"resource": "dashboardName"
}
```
If you wish to still embed a dashboard _but allow navigation between dashboards_, then your payload should include both parameters:
```json
{
"type": "explore",
"resource": "dashboardName",
"navigation": true
}
```
Finally, _if you wish to embed the project list view of dashboards instead (what you see when you first open a project in Rill Cloud)_, then you can simply omit the `resource` and appropriately set `navigation` in your payload:
```json
{
"navigation": true
}
```
## Testing the dashboard
While it is possible to create the iframeSrc URL via the CLI or code to _test_ your embedded dashboard, it might be easier to start off using [Rill Developer's mock users](/developers/build/metrics-view/security#advanced-example-custom-attributes-embed-dashboards), especially if you have multiple attribute views that you want to test before deploying to Rill Cloud. You can pass specific custom_attributes as you would during iframe URL generation to view the pre-filtered explore dashboard.
```yaml
- email: embed@rilldata.com
name: embed
custom_variable_1: Value_1
custom_variable_2: Value_2
```
## Frontend: Embed the dashboard
Your frontend should request an iframe URL from your backend API (which you set up in the previous step) and use the `iframeSrc` value of the response to render an HTML `