Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26)

### Bug Fixes

- **cubestore-driver:** don't fail queries on `write EPIPE`, report over-limit messages readably ([#11490](https://github.com/cube-js/cube/issues/11490)) ([d9ff741](https://github.com/cube-js/cube/commit/d9ff741b991ae9fb0bde5b742f6d514f3a7f94d7)), closes [#11655](https://github.com/cube-js/cube/issues/11655)
- **release:** repair lerna-publish.sh's release step and bump cleanup ([#11658](https://github.com/cube-js/cube/issues/11658)) ([a3bc548](https://github.com/cube-js/cube/commit/a3bc548ea1fffacccf6c77477cfb835b7d83429e))

### Features

- **cubesql:** Push UNION down to the data source ([#11651](https://github.com/cube-js/cube/issues/11651)) ([2aba3ac](https://github.com/cube-js/cube/commit/2aba3ac6d15b3f6885df952f3b0380dbbbd5a31d))

## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26)

### Bug Fixes
Expand Down
37 changes: 29 additions & 8 deletions docs-mintlify/admin/ai/multi-agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ Multi-agent is useful when:
- You want specialized agents with distinct instructions or tool access in the same deployment.
- You need to isolate context (rules, certified queries, memories) between user groups.

<Warning>
Spaces and agents must first be created through the Cube Cloud UI before they can be configured via YAML. The system matches YAML entries to UI-created spaces and agents by their `name` field.
</Warning>
<Info>
`agents/config.yml` is the source of truth for spaces and agents: declaring them there and [reconciling](#reconciliation) is the only supported way to create either. Agents can't be created in the UI, and their settings are read-only there. Cube links every record it creates to the entry it came from, and matches them by that entry's `name`.
</Info>

## Architecture

Expand Down Expand Up @@ -128,19 +128,40 @@ spaces:

Each agent must reference exactly one space via its `space` property. Multiple agents can share the same space and inherit its rules, certified queries, and memories.

## Reconciliation

A space or agent declared in `agents/config.yml` needs a matching record in Cube before users can chat with it. Cube creates those records from your config — that step is **reconciliation**:

<Steps>
<Step title="Declare the space and the agent in YAML">
Add the `spaces:` and `agents:` entries to `agents/config.yml`. In dev mode the pending list reflects your branch, so you can reconcile before merging.
</Step>
<Step title="Open the pending configurations">
Cube compares the config with the records already available to that deployment, matching each entry by `name`. Entries with no record yet are listed under **Pending Configurations** on the **Agents** page and on **Agents** → **Spaces**, once you pick the deployment. The Semantic Model IDE also shows a **Reconcile agent configs** button with the pending count that links there.
</Step>
<Step title="Create them">
Choose the [space scope](#space-scope) and press **Create All**. Spaces are created first, then each agent is linked to the space its `space` property names.
</Step>
</Steps>

Reconciliation only creates missing spaces and agents, so you need it only when an entry has no record available to the deployment yet: after adding a `name` to the config, and on each deployment you reconcile for the first time — an agent belongs to one deployment, as does a space created per deployment, while a global space counts as created everywhere. Agent behavior — `llm`, `description`, `accessible_views`, `memory_mode`, rules, certified queries — is read from the `agents/` directory of the deployment's data model and takes effect without reconciling. The implicit `auto` space and agent of the [single-agent setup](/admin/ai) are never listed as pending.

An agent's `space` is the exception: the link is made when the agent is created, so changing it in YAML doesn't move an existing agent. The agent's page flags the mismatch between the space its config names and the space it is linked to. If the newly named space has no record yet, it appears under **Pending Configurations**, and **Create All** creates it and moves the agent onto it in the same action. If that space is already available to the deployment, nothing about the agent is pending and **Create All** won't move it — delete the agent so its config reads as pending again, then **Create All** recreates it in the space the config names. The recreated agent is a new agent, so chats from before the delete don't carry over to it.

<Warning>
Changing an entry's `name` reads as a new entry: reconciling creates a new space or agent, and the record created from the old name stays behind with everything tied to it — an agent's chats, a space's memories — flagged **(misconfigured)** because its config no longer exists. Delete it from the **Agents** or **Spaces** page once you no longer need it.
</Warning>

## Space scope

Spaces live at the account level, so one space can be used by agents in more than one deployment. When a space is created, you choose its scope:

- **Global** — one space that agents in every deployment can use.
- **Per deployment** — the space is available to a single deployment only.

You pick the scope on the **Agents** → **Spaces** page, in two places:

- **Create space**, when you create a space by hand.
- The **Create All** panel, shown for spaces and agents declared in `agents/config.yml` that don't exist yet. Its **Create Spaces** control offers **Global** and **Per deployment**.
You pick the scope when you [reconcile](#reconciliation): the **Create All** panel's **Create Spaces** control offers **Global** and **Per deployment**. You can change the scope of an existing space later on its page, under **Agents** → **Spaces**.

Spaces created per deployment are named after the deployment, for example `Product (production)` and `Product (staging)`. Only the displayed name changes — the space stays linked to the space `name` declared in `agents/config.yml`, so the YAML entry keeps matching. You can change the scope of an existing space later on the space's page.
Spaces created per deployment are named after the deployment, for example `Product (production)` and `Product (staging)`. Only the displayed name changes — the space stays linked to the `spaces:` entry it was created from, so the YAML entry keeps matching.

Choose per-deployment scope when the same `spaces:` entry is declared in several deployments — typically development, staging, and production fed from branches of one data model — and you don't want them sharing what the space stores.

Expand Down
1 change: 1 addition & 0 deletions docs-mintlify/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,7 @@
"recipes/data-modeling/nested-aggregates",
"recipes/data-modeling/filtered-aggregates",
"recipes/data-modeling/share-of-total",
"recipes/data-modeling/average-order-value",
"recipes/data-modeling/period-over-period"
]
},
Expand Down
136 changes: 127 additions & 9 deletions docs-mintlify/docs/data-modeling/multi-fact-views.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,102 @@ The combined result shows measures from each fact table side by side:
Charlie has no orders and Diana has no returns — both are still included
with `NULL` values for the missing fact table.

## Combining facts in one measure

Putting measures from two facts side by side is often not the goal — you want a
single metric derived from both, such as revenue per order where revenue and
order count come from different fact tables. Neither cube can define it, because
neither can reference the other's measures.

Define it as a [measure of the view][ref-view-measures] instead, and mark it
[`multi_stage`][ref-multi-stage]:

<CodeGroup>

```yaml title="YAML"
views:
- name: customer_overview
cubes:
- join_path: orders
prefix: true
includes:
- count
- total_amount
- join_path: returns
prefix: true
includes:
- total_refund
- join_path: customers
includes:
- name
- city
- join_path: dates
includes:
- date

measures:
- name: refund_rate
type: number
multi_stage: true
sql: "{CUBE.returns_total_refund} / NULLIF({CUBE.orders_total_amount}, 0)"
```

```javascript title="JavaScript"
view(`customer_overview`, {
cubes: [
{
join_path: orders,
prefix: true,
includes: [`count`, `total_amount`]
},
{
join_path: returns,
prefix: true,
includes: [`total_refund`]
},
{
join_path: customers,
includes: [`name`, `city`]
},
{
join_path: dates,
includes: [`date`]
}
],

measures: {
refund_rate: {
type: `number`,
multi_stage: true,
sql: `${CUBE.returns_total_refund} / NULLIF(${CUBE.orders_total_amount}, 0)`
}
}
})
```

</CodeGroup>

`multi_stage` is what makes this work. It defers the expression to a stage that
runs _after_ the per-fact subqueries have been aggregated and joined, so the
division happens once per row of the combined result:

```sql
-- one aggregating subquery per fact, at the query's grain
SUM(orders.amount) GROUP BY city
SUM(returns.refund) GROUP BY city
-- final stage, once the two are joined on city
total_refund / NULLIF(total_amount, 0)
```

Without `multi_stage`, the same expression is planned as an ordinary calculated
measure. Cube then looks for a single join tree covering both fact cubes, finds
none — the facts only meet through the shared dimensions — and the query fails
with `Can't find join path to join …`, naming both facts. If you see that error
on a measure that spans facts, `multi_stage` is what's missing.

The measure is queried like any other, on its own or next to its components, and
grouped by any of the shared dimensions.

## Joining views in the SQL API

You don't have to define a dedicated multi-fact view to get multi-fact
Expand Down Expand Up @@ -407,17 +503,20 @@ GROUP BY 1, 2

### Filtering the join

Filters on top of the join are supported and are applied to the merged query:
Filters on top of the join are pushed into the merged query:

- A `WHERE` clause is pushed into the merged scan. A predicate on a dimension
shared by all facts filters the whole result; a predicate on a fact-specific
dimension filters only that fact's subquery.
- A `WHERE` clause is pushed into the merged scan, becoming a filter on the
member the predicate refers to.
- A predicate in the `ON` clause that the planner can attach to a single side
(for example, a condition on the optional side of a `LEFT JOIN`) becomes a
filter on that fact. Predicates that the SQL planner can't push to one side
of an outer join (such as a left-table condition in a `LEFT JOIN ON`) aren't
supported by the planner and will raise an error.

Pushing the predicate in is only the first step: the merged query is then
planned like any other multi-fact query, so the member it filters on must be
[shared by all facts](#filters-and-segments).

### Join type

The facts are stitched together with a `FULL JOIN` on the shared key, and the
Expand Down Expand Up @@ -518,24 +617,43 @@ the multi-fact join on the full set of common dimensions.
**Common dimension filters** (like `city = 'New York'` or `date > '2025-01-01'`)
are applied to every subquery, ensuring consistent filtering across all facts.

**Fact-specific filters** (like `orders.status = 'completed'`) are applied only
to that fact's subquery. Other fact subqueries remain unaffected.

**Measure filters** (like `orders_count > 1`) are applied as `HAVING`
conditions after the subqueries are joined.

[Segments][ref-segments] that belong to a specific fact table are applied only
to that fact's subquery.
**Fact-specific filters and [segments][ref-segments]** — anything that belongs to
one fact table rather than a shared dimension — can't be used in a multi-fact
query, however it is written: a `WHERE` clause in the SQL API, a filter in the
REST (JSON) API, or a segment. Every grouped dimension, filter and segment has
to be reachable from all facts, so a query that carries one fails with
`Can't find join path to join …`. The same members are fine as soon as only
that fact's measures are requested, since the query is no longer multi-fact.

To narrow one fact inside a multi-fact query, put the condition in the measure's
own [`filters`][ref-measure-filters] on its cube. It travels with the measure
into that fact's subquery and leaves the others alone:

```yaml
measures:
- name: completed_amount
sql: amount
type: sum
filters:
- sql: "{CUBE}.status = 'completed'"
```

## Join path requirements

- Each fact cube must declare **direct joins** to all shared dimension tables
- Dimension tables should be included in the view at **root-level join paths**,
not nested under a specific fact (e.g., `customers`, not `orders.customers`)
- Use `prefix` on fact cubes to disambiguate identically named members
- Everything a multi-fact query groups or filters by must be shared by all facts

[ref-views]: /docs/data-modeling/views
[ref-view-ref]: /reference/data-modeling/view
[ref-segments]: /reference/data-modeling/segments
[ref-measure-filters]: /reference/data-modeling/measures#filters
[ref-multi-stage]: /reference/data-modeling/measures#multi_stage
[ref-view-measures]: /reference/data-modeling/view#measures
[ref-sql-api]: /reference/core-data-apis/sql-api
[link-tesseract]: https://cube.dev/blog/introducing-tesseract
49 changes: 49 additions & 0 deletions docs-mintlify/docs/data-modeling/views.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,51 @@ control which members are exposed, how they're named, and how they're
organized. This keeps your model [DRY][wiki-dry] and makes maintenance
straightforward.

### Define a metric on a view when it spans cubes

The one exception is a metric whose parts live in different cubes, so there is
no single cube it could belong to. A view can define its own
[measures][ref-view-measures] and [dimensions][ref-view-dimensions] as long as
their `sql` only combines members the view already includes — a member that
reads a column instead is rejected at compile time:

<CodeGroup>

```yaml title="YAML"
views:
- name: orders_overview
# cubes: … includes orders.total_amount and line_items.count

measures:
- name: average_line_value
type: number
multi_stage: true
sql: "{CUBE.total_amount} / NULLIF({CUBE.count}, 0)"
```

```javascript title="JavaScript"
view(`orders_overview`, {
// cubes: … includes orders.total_amount and line_items.count

measures: {
average_line_value: {
type: `number`,
multi_stage: true,
sql: `${CUBE.total_amount} / NULLIF(${CUBE.count}, 0)`
}
}
})
```

</CodeGroup>

[`multi_stage`][ref-multi-stage] matters whenever the parts come from different
cubes: it aggregates each of them before combining, instead of evaluating the
expression inside one joined scan where a `one_to_many` join between them would
inflate the numerator. If the cubes don't join to each other at all, see
[multi-fact views][ref-multi-fact-views]. The full example, with the `cubes`
block, is on the [view reference][ref-view-measures].

### Control visibility

Not every view should be publicly accessible. Use [`public`][ref-view-public]
Expand Down Expand Up @@ -450,6 +495,10 @@ parameters.
[ref-view-description]: /reference/data-modeling/view#description
[ref-view-title]: /reference/data-modeling/view#title
[ref-view-public]: /reference/data-modeling/view#public
[ref-view-measures]: /reference/data-modeling/view#measures
[ref-view-dimensions]: /reference/data-modeling/view#dimensions
[ref-multi-stage]: /reference/data-modeling/measures#multi_stage
[ref-multi-fact-views]: /docs/data-modeling/multi-fact-views
[ref-view-folders]: /reference/data-modeling/view#folders
[ref-access-policies]: /reference/data-modeling/data-access-policies
[ref-ai-context]: /docs/data-modeling/ai-context
Expand Down
23 changes: 18 additions & 5 deletions docs-mintlify/docs/pre-aggregations/cube-store-architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1018,13 +1018,26 @@ A cache entry at or above `CUBESTORE_CACHE_MAX_ENTRY_SIZE` is rejected explicitl
with `Unable to SET cache with '<key>' key, exceeds maximum allowed size for
payload: <size>, max allowed: <limit>`.

<Warning>
A message above the *transport* limit is answered with an error naming the
size and the limit, and the connection stays up for the other queries
multiplexed over it:

**Known limitation.** A message above the *transport* limit is not answered
with an error. The deployment then reports `ConnectionError: CubeStore connection error:
write EPIPE`.
```
Request of 70000000 bytes exceeds the maximum message size of 67108864 bytes.
Reduce the size of the query, e.g. by sending fewer or smaller inline tables,
or raise CUBESTORE_TRANSPORT_MAX_MESSAGE_SIZE.
```

That holds up to twice whichever of the two transport limits above is lower.
A message beyond it is refused before it can be attributed to a query, so the
connection is closed instead and the other queries multiplexed over it are
re-sent on a new one. The close names the limit that refused it, so it says
which of the two to raise.

</Warning>
A result too large for Cube's own limit is reported the same way, as
`Cube Store response size exceeds the maximum message size of 100 MB`. That
limit is [`CUBEJS_CUBESTORE_MAX_MESSAGE_SIZE`](/reference/configuration/environment-variables#cubejs_cubestore_max_message_size),
which is independent of the Cube Store side and defaults to 100 MB.

If a query hits this limit, reduce the size of its result set — add filters
or a lower `limit`, drop dimensions, or split it into several queries.
Expand Down
Loading
Loading