> For the complete documentation index, see [llms.txt](https://help.tillit.cloud/tillit/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://help.tillit.cloud/tillit/knowledge-base/setting-up-tillit/activity-templates/activity/querying-cubejs-in-scripts.md).

# Querying Cube.js in Scripts

Query aggregated data from Cube.js inside an OnLoad or Calculation Script, to show a live number on a form calculated from historical records.

An [OnLoad or Calculation Script](/tillit/knowledge-base/setting-up-tillit/activity-templates/activity/scripts.md) can pull in more than a single record — it can query aggregated data across many activity recordings using Cube.js, TilliT's analytics engine (the same one behind [Dashboards](/tillit/knowledge-base/reports/dashboards.md)).

{% hint style="info" %}
Cube.js is read-only and built for aggregation — an average, sum, or count grouped by order, asset, or another dimension. For a single record, a plain list, or anything that creates, updates, or deletes, use the regular TilliT API instead.
{% endhint %}

### Querying a Cube

A Cube.js query always has the same three ingredients: `measures` (what to aggregate), `dimensions` (what to group by), and `filters` (what to scope to). Call it through `cube/v1/load` from an OnLoad Script:

```javascript
const params = {
  query: JSON.stringify({
    measures: ['ActivityInstanceItem.sumValue', 'ActivityInstanceItem.count'],
    dimensions: [],
    timeDimensions: [],
    filters: [
      {member: 'ActivityInstance.activityKey', operator: 'equals', values: ['ExampleActivityKey']},
      {member: 'ActivityInstanceItem.orderNumber', operator: 'equals', values: [`${$form.order.orderNumber}`]}
    ]
  })
};
const result = await $tillitApi.get('cube/v1/load', params);
$scope.result = result;
```

Scope the query to what you actually need with `filters` — filtering by the current order in the query itself, rather than grouping by every order with `dimensions` and searching the response afterward, means Cube.js only ever computes and returns the rows you want.

Read the result back out in a Calculation Script on another item, the same way as any other `$scope` value:

```javascript
(() => {
  const row = $scope.result?.data?.[0];
  if (!row) return 'Empty';
  const sum = parseFloat(row['ActivityInstanceItem.sumValue']);
  const count = parseInt(row['ActivityInstanceItem.count']);
  return count > 0 ? String(sum / count) : 'Empty';
})()
```

{% hint style="warning" %}
The `$scope` and wrapping rules for OnLoad and Calculation Scripts apply here exactly as documented on the [Scripts](/tillit/knowledge-base/setting-up-tillit/activity-templates/activity/scripts.md) page — this OnLoad Script must not be wrapped in its own async function, and the Calculation Script above must be.
{% endhint %}

### Finding real field names

Every measure and dimension a query can reference is defined per entity in the backend's cube schema — you can't query an aggregate that isn't already defined there. The Dashboard element editor's Measure and Dimension pickers are a friendlier way to browse what's available, but aren't always complete; if a field you expect isn't showing up in one picker, check the other before concluding it doesn't exist.

### Troubleshooting

* **Filtering on a name pattern** (e.g. matching an item's key or name with `contains`) works, but ties the query to a naming convention — renaming the field, or adding a differently-named field recording the same kind of value, silently stops it being picked up. A tagged classification, like filtering on a Process Variable, survives renames better — but only catches rows recorded **after** that Process Variable was created and assigned. Confirm when a Process Variable was introduced before relying on it for anything that needs to be complete against older data.
* Filtering across entities (e.g. filtering `ActivityInstanceItem` by a field that lives on `OrderInstance`) only works if the two are actually joined in the cube schema — prefer a field the cube already proxies for you, such as `ActivityInstanceItem.orderNumber`, over reaching across a join directly.
* An empty result (`"data": []`) means nothing matched — not an error. Check the filter values are exactly correct (the real `activityKey`, not a guess from a display name) and that the record you're scoping to has any qualifying data at all.
