Put Rills metrics on the same screen as the rest of your GTM reporting: leads waiting for outreach, outbound runs by status, approval queue depth, and decision volume. Grafana reads them through the query API using the Infinity data source plugin, so no Rills database access is involved.
This guide uses a scoped Rills API key stored in the data source’s secure settings. That’s the setup Rills currently supports for Grafana. Other sign-in options may be added later; this page will be updated if they are.
#Before you start
- A Grafana instance (self-hosted or Grafana Cloud) where you can install plugins and add data sources. Plan features and free-tier limits are set by Grafana and can change.
- The Infinity data source plugin installed.
- A Rills workspace admin or owner to create the key.
#1. Create a key for the dashboard
In Settings → API & MCP access, create a key named after the dashboard and grant only what its panels read:
| Panels | Scopes |
|---|---|
| Workflow runs and durations | workflows:read |
| Approval queue and decisions | approvals:read |
| Lead or pipeline counts from a table | tables:read, payloads:read |
| Form submission volume | forms:read, payloads:read |
A run-health and approval dashboard doesn’t need payloads:read. Copy the key when it’s shown; you’ll paste it into Grafana once and nowhere else. See Authentication.
#2. Test the key with cURL
Confirm the key and scopes before touching Grafana:
curl --fail-with-body \
--request POST \
--header "Authorization: Bearer $RILLS_API_KEY" \
--header "Content-Type: application/json" \
--data '{"dataset":"approvals","filters":[{"field":"status","op":"eq","value":"pending"}],"measures":[{"alias":"pending","fn":"count"}]}' \
"https://rills.ai/api/v1/query"
A working key returns {"data":[{"pending":...}],"meta":...}. A 401 means the key is wrong or revoked; a 403 names the missing scope.
#3. Add the data source
In Grafana, add a new Infinity data source:
- Under Authentication, choose Bearer Token and paste the key. Grafana stores it encrypted and never sends it to viewers’ browsers.
- Under Allowed hosts, add
https://rills.aiso the key is only ever sent to Rills. - Set the base URL to
https://rills.ai/api/v1. - Save and test.
Configure the key here, never inside a panel query, so it stays out of dashboard JSON and exports.
If you provision Grafana from files, keep the key in an environment variable:
apiVersion: 1
datasources:
- name: Rills API
uid: rills-api
type: yesoreyeram-infinity-datasource
jsonData:
auth_method: bearerToken
url: https://rills.ai/api/v1
allowedHosts:
- https://rills.ai
secureJsonData:
bearerToken: ${RILLS_API_KEY}
#4. Build a panel
Every Rills panel is a POST to /query. In the panel’s query editor:
| Setting | Value |
|---|---|
| Type | JSON |
| Parser | Backend |
| Source | URL |
| Format | Table (stat and table panels) or Time Series |
| Method | POST |
| URL | https://rills.ai/api/v1/query |
| Body type | Raw, content type JSON |
| Root selector | data |
Use the Backend parser. It keeps requests and the key on the Grafana server and is required for alerting.
To follow the dashboard’s time picker, use Grafana’s time macros in timeRange:
{
"dataset": "workflowExecutions",
"resourceId": "00000000-0000-4000-8000-000000000000",
"timeRange": { "from": "${__from:date:iso}", "to": "${__to:date:iso}" },
"timeBucket": { "field": "createdAt", "interval": "day" },
"groupBy": ["status"],
"measures": [{ "alias": "runs", "fn": "count" }],
"orderBy": [{ "field": "bucket", "direction": "asc" }]
}
Then add columns: bucket as Timestamp, status as String, and runs as Number. Results come back in long format (one row per day and status). To draw one line per status, add Grafana’s Partition by values transformation on status.
Keep the dashboard range within 366 days and choose a bucket interval that fits it. A year of minute buckets is far more rows than the 1,000-row limit.
#Starter panels
The bodies for these panels are in Query workspace data. Replace each fixed timeRange with the macros above.
| Panel | Visualization | Query |
|---|---|---|
| Leads not contacted | Stat | tableRows, eq filter on your contact-status field, count |
| Daily outbound runs | Time series | workflowExecutions for one workflow, day bucket, grouped by status |
| Run duration | Time series | workflowExecutions, day bucket, avg and p95 of durationMs |
| Pending approvals | Bar chart or table | approvals, status eq pending, grouped by priority |
| Approval outcomes | Time series | approvals, day bucket on decidedAt, grouped by decisionKind |
| Form submissions | Time series | formSubmissions, day bucket, count |
For Pending approvals, the range filters on when requests were created. Give the panel its own longer window in Query options → Relative time (for example 30d) so older pending requests still count.
#5. Set the refresh interval
You choose the refresh interval, within the API’s rate limits; there is no unlimited refresh. Each panel costs 10 request units per refresh against a budget of 1,200 per minute per key.
- Start at 30 seconds.
- For multi-panel dashboards, stay at 10 seconds or slower.
- Before going faster, multiply panels by refreshes per minute by 10 and compare it with the
RateLimit-Remainingheader. - Use a separate key per dashboard so one busy screen can’t throttle another.
#Troubleshooting
| You see | Cause | Fix |
|---|---|---|
| No data | The query matched nothing. Grouped and bucketed queries return an empty array. | Widen the time range or check filter values. |
401 | Key wrong, deleted, or pasted with extra spaces. | Create a new key and update the data source. |
403 | Key lacks a scope the dataset needs. | Edit the key’s scopes in Rills. |
400 QUERY_TOO_EXPENSIVE | Missing time range, range over 366 days, or an unsupported field or operator. | See query rules. |
429 | Refresh too fast for the number of panels. | Increase the refresh interval or split dashboards across keys. |
503 QUERY_TIMEOUT | The query ran past 5 seconds. | Shorten the range or remove a grouping. |