Skip to content

Agent Spaces

Agent Spaces are named, admin-authored personas that scope an AI ask. Where Agent Skills codify individual workflows, a space codifies a viewpoint: system prompt, cube allowlist, skill allowlist, suggested prompts — enforced server-side so no matter what the caller sends, the LLM sees the persona voice + the persona’s cubes + the persona’s skills.

Persisted as JSON under saiku-home/agent-spaces/. The launcher scans lazily on mtime signature (same model as skills — no watcher thread).

Shipped in saiku v4.7 as saiku#1440.

What a space enforces

File format

saiku-home/agent-spaces/foodmart-sales-analyst.json
{
"id": "foodmart-sales-analyst",
"name": "FoodMart Sales Analyst",
"description": "Weekly and monthly sales rollups over the FoodMart Sales cube.",
"systemPrompt": "You are the FoodMart Sales Analyst. Prefer weekly and monthly time grain unless the user asks otherwise. Lead with the top three lines by absolute value. Flag any figure that swings by more than 20% versus the prior period. Be analytical, brief, and numbers-first.",
"cubeAllowlist": [
{"connectionName": "unknown_foodmart", "catalog": "FoodMart", "schema": "FoodMart", "cubeName": "Sales"}
],
"skillAllowlist": ["weekly-foodmart-rollup"],
"suggestedPrompts": [
"How did Store Sales track last week vs the prior week?",
"Break down Store Sales by Product Family for Q4.",
"/weekly-foodmart-rollup"
]
}
FieldRequiredTypeNotes
idyesstringkebab-case, [a-z][a-z0-9-]{0,63}. Path segment in /ai/spaces/{id}/ask.
nameyesstringDisplay name for the catalogue and sidebar.
descriptionnostringOne-line summary shown in the space picker.
systemPromptnostringPrepended to the built-in SYSTEM_PROMPT on every ask.
cubeAllowlistyesAiCubeRef[]At least one entry. Refs outside this list return 403 FORBIDDEN.
skillAllowlistnostring[]Filters slash-routing + LLM catalogue. Empty = all skills allowed.
suggestedPromptsnostring[]Free-form quick-start questions the UI can render.

Unknown top-level keys are rejected so a typo (sytemPrompt) surfaces as UNKNOWN_FIELD rather than being silently dropped.

Cube enforcement

  1. Caller POSTs to /ai/spaces/{id}/ask with an optional cube field:

    Terminal window
    curl -sS -X POST -H 'Content-Type: application/json' \
    -u admin:admin \
    http://localhost:8080/saiku/api/ai/spaces/foodmart-sales-analyst/ask \
    -d '{"question": "How did Store Sales track last week?"}'
  2. If cube is omitted, the space’s first allowlisted entry is used.

  3. If cube is supplied, it must match a space allowlist entry on all four coordinates (connectionName, catalog, schema, cubeName) or the call returns:

    HTTP 403 Forbidden
    {
    "degraded": true,
    "reason": "FORBIDDEN: cube OtherCube is not in space 'foodmart-sales-analyst' allowlist"
    }

This is deliberate: a UI that hands the server a stale cube ref should get corrected, not silently reinterpreted onto the default.

System prompt injection

The space’s systemPrompt is prepended to the built-in SYSTEM_PROMPT on the provider side. The full assembled system message the LLM sees:

You are a Mondrian OLAP analyst assistant scoped to a single cube. …
[…the built-in tool-choice rails…]
Agent space persona:
You are the FoodMart Sales Analyst. Prefer weekly and monthly time
grain unless the user asks otherwise. Lead with the top three lines by
absolute value. Flag any figure that swings by more than 20% versus the
prior period. Be analytical, brief, and numbers-first.
Cube schema:
{ … the AiSchema JSON … }
Cube ref to echo: { … }

The user’s history and question fields ride below all of that as regular messages — nothing they can inject re-writes the persona.

Skill filter

The skill catalogue is filtered to the space’s skillAllowlist before it reaches the LLM:

  • The skillAllowlist is empty → all skills flow through.
  • The skillAllowlist names specific skills → only those show up in the LLM system prompt AND only those slash-expand.

An ask like /some-other-skill for Q4 in a space that doesn’t allowlist some-other-skill falls through as a raw ask — the LLM sees the message literally, with no expansion. The routing decision is one-sided (allowlist deny), never partial.

Suggested prompts

Every space carries a suggestedPrompts list — 3-6 authored quick-start questions the UI surfaces as chips:

"suggestedPrompts": [
"How did Store Sales track last week vs the prior week?",
"Break down Store Sales by Product Family for Q4.",
"Which Product Department is up the most month-over-month?",
"/weekly-foodmart-rollup"
]

Slash-command entries are legal and encouraged — a suggested prompt that starts with / invokes the named skill directly.

REST surface

GET /rest/saiku/api/ai/spaces

Catalogue of {id, name, description, suggestedPrompts} summaries.

Terminal window
curl -sS -u admin:admin \
http://localhost:8080/saiku/api/ai/spaces | jq
{
"spaces": [
{
"id": "foodmart-sales-analyst",
"name": "FoodMart Sales Analyst",
"description": "Weekly and monthly sales rollups over the FoodMart Sales cube.",
"suggestedPrompts": ["How did Store Sales track last week?", ""]
}
]
}

GET /rest/saiku/api/ai/spaces?errors=true

Same, plus an errors[] array. Stable error codes:

CodeWhen
EMPTY_SPACEFile is empty or all whitespace.
MALFORMED_JSONJSON parser rejected the body.
MISSING_FIELDRequired field (id, name) not present.
BLANK_FIELDRequired field present but empty / whitespace-only.
TYPE_MISMATCHField present but wrong type.
INVALID_IDid doesn’t match [a-z][a-z0-9-]{0,63}.
EMPTY_ALLOWLISTcubeAllowlist present but empty — space would be unusable.
INVALID_CUBE_REFAllowlist entry missing a coordinate (connectionName, etc.).
UNKNOWN_FIELDFrontmatter contains a field not in the schema.
DUPLICATE_IDTwo files declared the same id.
IO_ERRORCould not read the file (filesystem-level).

GET /rest/saiku/api/ai/spaces/{id}

Full record — includes systemPrompt and cubeAllowlist that the summary omits. Used by the admin UI when editing a persona.

POST /rest/saiku/api/ai/spaces/{id}/ask

Space-scoped ask. Body shape mirrors /ai/ask but the cube field is optional:

Terminal window
curl -sS -X POST -H 'Content-Type: application/json' \
-u admin:admin \
http://localhost:8080/saiku/api/ai/spaces/foodmart-sales-analyst/ask \
-d '{"question": "Break down Store Sales by Product Family for Q4"}'

Response envelope is the standard AiAskApi.AskResponsemodel, request (the AiQueryRequest the model emitted), and degraded/reason on error.

POST /rest/saiku/api/ai/spaces/{id}/ask/stream

Streaming variant of the space-scoped ask. Emits the same Server-Sent Events schema as /ai/ask/stream (modelintentchunkfinal) with the persona scope applied — the client sees identical wire events whether it hits /ai/ask/stream or this space-scoped mirror. Space-not-found and out-of-allowlist cube outcomes surface as an error event followed by a degraded final, so the reader handles scope failures and provider failures the same way.

Terminal window
curl -sS -N -X POST -H 'Content-Type: application/json' \
-u admin:admin \
http://localhost:8080/saiku/api/ai/spaces/foodmart-sales-analyst/ask/stream \
-d '{"question": "Break down Store Sales by Product Family for Q4"}'

POST /rest/saiku/api/ai/spaces/refresh

Force a rescan (bypasses the mtime signature check).

Authoring spaces in the admin panel

Spaces no longer have to be hand-authored JSON. Admin → Agent spaces is a full editor: write the system prompt, tick the cubes in the allowlist (backed by live cube discovery, so you can only allow cubes that exist), list the skills and suggested prompts, and save. Deleting a space removes its JSON file.

The editor is backed by an admin-gated CRUD surface. These endpoints require ROLE_ADMIN and return the full persona (system prompt + cube allowlist included, unlike the redacted public catalogue above):

Method + pathPurpose
GET /rest/saiku/admin/agent-spacesList every persona in full, for editing.
GET /rest/saiku/admin/agent-spaces/errorsParse errors for malformed files.
PUT /rest/saiku/admin/agent-spaces/{id}Create or replace a persona. The id is validated (kebab-case, path-traversal guarded) before the JSON file is written.
DELETE /rest/saiku/admin/agent-spaces/{id}Remove a persona and its file.

Embedding a space

<saiku-embed kind="ai" space="foodmart-sales-analyst"> drops a persona-scoped assistant into any page. The cube allowlist and system prompt are enforced server-side exactly as they are for the REST ask, so an embedded assistant can’t be steered off its persona. See the embed guide.

Bundled examples

Fresh Saiku installs stage two working personas:

  • FoodMart Sales Analyst — analytical, brief, numbers-first. weekly-foodmart-rollup in its skill allowlist so /weekly-foodmart-rollup is available as a slash command.
  • FoodMart Finance Ops — cautious, precise, margin-focused. Empty skillAllowlist = all skills allowed.

Both scope to the FoodMart Sales cube — a fresh demo has personas ready to click without any operator authoring.

Non-goals for v1

  • Per-user or per-role scoping. Spaces are per-launcher in v1; multi-tenant workspaces can layer scoping by mapping workspace directories onto per-workspace registry roots — deferred.
  • Per-space data-scope override. Embedded queries already enforce forced row-level filters (apply-or-fail-closed — see the embed guide), but pinning RLS filters onto space-scoped asks specifically is still tracked with the Ossie RLS work in saiku#1393.

Where to go next