Skip to content

Agent Skills

Agent Skills are markdown files with YAML frontmatter that live in saiku-home/skills/. The launcher scans them lazily on every request and injects the catalogue into the LLM system prompt, so any AI Ask turn — MDX or Ossie — can route to an admin-authored workflow instead of freewheeling.

Small feature, disproportionate value: it lets operators codify their team’s canonical questions (“this quarter’s exec rollup”, “the churn cohort”, “the store-comp report”) without owning yet another tool. The skill lives in the repo, versions with the semantic model, and gets code-reviewed like everything else.

Shipped in saiku v4.7 as saiku#1426.

Two invocation paths

File format

saiku-home/skills/weekly-foodmart-rollup.md
---
name: weekly-foodmart-rollup
description: |
Weekly revenue rollup for the FoodMart Sales cube: total Store Sales
by Product Family for the last 7 days, compared to the prior 7 days.
Flags any family with a >20% swing.
cube: unknown_foodmart/FoodMart/FoodMart/Sales
---
## Steps
1. Query total `[Measures].[Store Sales]` and `[Measures].[Unit Sales]`
broken down by `[Product].[Product Family]` for the last 7 days
(`[Time].[Weekly].[Week].&[latest]`).
2. Query the same shape for the prior 7 days
(`[Time].[Weekly].[Week].&[latest - 1]`).
3. Present the result as a two-column table with a `Δ vs prior` percentage
column derived per family.
4. Highlight any Product Family whose `Δ vs prior` swings by more than 20%
in either direction.

Frontmatter

FieldRequiredTypeNotes
nameyesstringkebab-case, [a-z][a-z0-9-]{0,63}. Used as the slash slug.
descriptionyesstringOne line or block scalar. Shown in /ai/skills and the LLM prompt.
cubenostringconnection/catalog/schema/cubeName ref. Scopes the skill.

Unknown top-level keys are rejected. A typo (descripton) surfaces as a structured UNKNOWN_FIELD error — never as a silent nameless skill.

Body

Everything after the closing --- fence is the body. Markdown is recommended but not required; the body is treated as opaque text and pasted verbatim into the LLM prompt on a slash-command match. Keep it tight — the LLM has to read the whole file.

Slash command

  1. User (or the DimSum widget on your behalf) posts an ask starting with /:

    Terminal window
    curl -sS -X POST -H 'Content-Type: application/json' \
    -u admin:admin \
    http://localhost:8080/saiku/api/ai/ask \
    -d @- <<'EOF'
    {
    "question": "/weekly-foodmart-rollup for Q4 instead of this week",
    "cube": {
    "connectionName":"unknown_foodmart",
    "catalog":"FoodMart",
    "schema":"FoodMart",
    "cubeName":"Sales"
    }
    }
    EOF
  2. The service parses the slash and looks up weekly-foodmart-rollup in the catalogue.

  3. Match → the ask sent to the LLM is:

    Skill: weekly-foodmart-rollup
    ## Steps
    1. Query total [Measures].[Store Sales] …
    User follow-up: for Q4 instead of this week
  4. The LLM runs the steps, applies the follow-up (“Q4 instead of this week”) to the time filter, and emits an AiQueryRequest as usual.

  5. Miss → the ask travels unchanged. The LLM sees it as an ordinary prompt with a leading slash. Nothing breaks.

Natural language

The catalogue lands in the LLM system prompt as:

Available skills (admin-authored workflows). When the user's question
closely matches one of these — or when the message starts with
`/<skill-name>` — use the skill's steps to structure your response
instead of freewheeling:
- /weekly-foodmart-rollup: Weekly revenue rollup for the FoodMart Sales cube…
- /store-comp-report: Store-level comp vs same period last year…
- /churn-cohort: 30/60/90 day churn cohort split by acquisition channel…

The model routes on its own. If the user asks “how did stores compare to last year?”, the natural-language matcher picks /store-comp-report; the user never sees the routing decision.

REST surface

All three endpoints sit under the standard AI Ask base path.

GET /rest/saiku/api/ai/skills

The catalogue — compact {name, description, cube} summaries.

Terminal window
curl -sS -u admin:admin \
http://localhost:8080/saiku/api/ai/skills | jq
{
"skills": [
{
"name": "weekly-foodmart-rollup",
"description": "Weekly revenue rollup for the FoodMart Sales cube: total Store Sales …",
"cube": "unknown_foodmart/FoodMart/FoodMart/Sales"
}
]
}

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

Same shape, plus a errors[] array listing every file that failed to parse this scan. Each entry carries a stable machine code so operators fix bad frontmatter without reading server logs.

{
"skills": [ /* … */ ],
"errors": [
{
"path": "broken.md",
"code": "MISSING_FRONTMATTER",
"message": "expected leading `---` YAML frontmatter block"
}
]
}

Stable error codes:

CodeWhen
EMPTY_SKILLFile is empty or all whitespace.
MISSING_FRONTMATTERNo leading --- fence.
EMPTY_BODYFrontmatter parsed but no markdown after the closing fence.
MALFORMED_YAMLYAML parser rejected the frontmatter.
MISSING_FIELDRequired field (name / description) not present.
BLANK_FIELDRequired field present but empty / whitespace-only.
TYPE_MISMATCHField present but wrong type (name: 42).
INVALID_NAMEname doesn’t match [a-z][a-z0-9-]{0,63}.
UNKNOWN_FIELDFrontmatter contains a field not in the schema.
DUPLICATE_NAMETwo skill files declared the same name.
IO_ERRORCould not read the file (filesystem-level).

GET /rest/saiku/api/ai/skills/{name}

Full body of one skill — the raw markdown. Handy for a UI slash-menu that previews the workflow before the user hits send.

POST /rest/saiku/api/ai/skills/refresh

Force a rescan (bypasses the mtime signature check). Returns the fresh counts so operators can eyeball the reload.

Terminal window
curl -sS -u admin:admin -X POST \
http://localhost:8080/saiku/api/ai/skills/refresh
{ "skills": 4, "errors": 0 }

Bundled example

Fresh Saiku installs stage a working example on first boot — see weekly-foodmart-rollup.md. Operators add their own alongside; the seeded one is idempotent (only lands when the target file doesn’t already exist).

How the scan works

  • Subdirectories are walked recursively.
  • Non-.md files are ignored (a stray README.txt won’t crash the scan).
  • Broken files don’t take down the catalogue — a ParseException on one file leaves the others intact.
  • Duplicates on name produce a DUPLICATE_NAME error on the second file to load; the first one wins.

Ossie asks pick these up too

The skill catalogue is a single per-launcher store served at /rest/saiku/api/ai/skills. Any skill whose cube: field names an Ossie ref (e.g. pharma/Pharma/Pharma/Sales) is naturally scoped by that ref when routed through /ai/ossie/ask. MDX and Ossie share the same primitive.

Where to go next