Virtual Dimensions
A virtual dimension is a custom cost axis (e.g. "Environment", "Team", "Product line") built from ordered rules. Each rule has:
conditionCel — CEL filter selecting rows (e.g. cos_environment in ["prod"])
allocation — where matched spend goes. Supports all types: dimensionValue, existingColumn, splitCost, telemetry (same shapes as get returns for a virtual dimension)
Rules are evaluated top-to-bottom; first match wins. A leftover rule catches everything else.
Edits happen on a draft. Publishing is explicit and triggers BigQuery refresh. Production is unchanged until publish_virtual_dimension.
When to Trigger
- Building a new custom cost axis (Environment, Team, Product line, …)
- Editing rules on an existing virtual dimension
- Previewing leftover %, breakdown, or rule overlap/shadowing
- Splitting shared cost by a usage metric (
telemetry allocation)
- Publishing a draft after user confirmation
Auth
Write tools (create / update / publish) are Clerk-only — not available on the service route. Read tools work on both routes.
Concepts
| Concept |
Meaning |
| Draft vs published |
Mutations touch the latest pending draft; published state stays live until publish |
| Draft persistence |
preview_virtual_dimension_draft and virtual_dimension_overlap_matrix (when includeDraft: true) return draftPersisted: true when a pending draft exists, draftPersisted: false when analyzing published-only state in memory (not publishable). create_virtual_dimension_draft and update_virtual_dimension_draft persist only when validation passes — on success draftPersisted: true and draftValidation: { ok: true }; on failure nothing is saved (draftPersisted: false). publish_virtual_dimension requires a persisted pending draft — call update_virtual_dimension_draft for an existing virtualDimensionId (or create_virtual_dimension_draft for a brand-new VDIM) |
| Rule order |
Earlier rules claim spend; later overlapping rules are shadowed |
| Leftovers |
Spend not matched by any named rule — high leftover means add more rules |
| Leftover (MCP) |
Auto catch-all returned as leftoverRule (not in rules). Do not include a catch-all in rules on create or update — duplicate leftovers cause ordering confusion. There is no leftover-label input on create/update — rename the leftover bucket in the web app |
| Rule ids |
Create: saveVirtualDimensionDraft re-stamps all rule ids — response ids are authoritative. Update: preserves ids from input (including when seeding a draft from published state). Both: ids are sticky to logical rules — reorder by moving id + name + conditionCel + allocation together; attaching an id to a different condition silently mis-assigns spend |
| Tags |
get returns tags (names) for both published and draft. Pass tagNames (strings) on create/update — not tag UUIDs |
values |
Output-only — derived from rule allocations; do not send on create/update |
| Allocation shapes |
dimensionValue: { allocationType: "dimensionValue", dimensionValue: "prod" }; existingColumn: { allocationType: "existingColumn", existingColumn: "cos_team" }; splitCost: { allocationType: "splitCost", reAllocationParams: { type: "custom", partitions: [{ label: "A", weight: 60 }, { label: "B", weight: 40 }] } } (weights sum to 100); telemetry: { allocationType: "telemetry", externalMetric: { provider, integrationId, metricName, aggregator, groupByFields: ["<split-by>"] }, mappingType: "mapping" | "regexMapping" | "identity", mappingParams: { mapping: { "key": "value" } }, regexTransformation: "..." } — discover externalMetric via list_metrics { includeExternal: true, search }. Do not set datasource on new reallocations (leftover stored rows may still have it — echo unchanged). mapping keys are metric value names (not cos_* dimensions); unmapped values → leftover. Echo non-dimensionValue allocations from get unchanged unless the user explicitly asks to edit advanced allocation behavior |
| CEL only |
Always use conditionCel strings; never raw query-builder JSON |
| Unlabelled values |
Use cos_environment == null in conditionCel (not is_null or string "null") |
| Declarative updates |
update_virtual_dimension_draft takes the full desired rules array, not diffs |
name vs bqName |
name is the display label (can change in drafts). bqName is the immutable BigQuery / CEL field name (e.g. virtual_environment) — set once at create_virtual_dimension_draft from the initial name and never updated, even after rename or publish. Read bqName from create/update/publish/get responses (or list_virtual_dimensions when resolving by name only) before groupBy / filterCel; never derive it from name. |
| Post-publish query |
After publish, use returned bqName (not name) in query. computeStatus: REFRESHING = refresh job queued — poll get or list_virtual_dimensions until COMPLETED before querying; TO_REFRESH = draft promoted but refresh job queuing failed — retry publish_virtual_dimension or poll computeStatus until COMPLETED |
Strategy approval ≠ rule approval
When several mapping approaches exist (e.g. reuse env vs derive from cos_sub_account_id patterns vs split by cos_team), present the options and let the user pick a strategy first.
Picking a strategy is not approval to create the draft. After the user chooses an approach:
- Draft the concrete rules for that strategy — each rule's
name, conditionCel, and allocation (leftover catch-all is added automatically; do not put it in rules).
- Present the full proposed rule set — or, when there are many rules, a representative summary (rule order, main buckets, example CEL per bucket) plus called-out edge cases (null/unlabelled values, ambiguous values, known shadowing risks, spend you could not classify).
- Wait for explicit rule approval — the user must confirm the rules (or request edits) before you call
create_virtual_dimension_draft.
Do not call create_virtual_dimension_draft immediately after the user says "use approach B" or "map by sub-account" — that only approves the mapping strategy, not the rule set.
When target values are unclear
If you do not know what buckets the finished dimension should have — vague ask ("organize our cloud spend"), no obvious cos_* field, or search results do not suggest a sensible split — ask the user for the values they expect before proposing mapping strategies or rules.
Example prompt: "What values should this dimension use? For example: Data, ML, Infra — or prod, staging, dev?"
Use their answer as:
- Target
allocation.dimensionValue labels (one rule per bucket, unless they specify otherwise)
- Keywords for
search and query when drafting conditionCel
- A checklist when presenting the proposed rule set (did every expected value get a rule? what lands in leftover?)
Do not invent bucket names from spend patterns alone without confirming — inferred labels often miss how the org actually thinks about costs. Batch value questions with other clarifying questions in a single message when possible.
Supporting tools (before VDIM tools)
| When |
Tool |
Discover valid cos_* dimension names |
search with type: ["dimensions"] (empty query lists all) |
| Find dimension values / existing VDIMs for a topic |
search with type: ["dimensions", "virtual_dimensions"] |
| Explore spend to decide what rules to write |
query |
| Pick a natural split dimension for unclaimed spend |
suggest_groupby |
Discover a live integration metric for a new telemetry allocation |
list_metrics { includeExternal: true, search } — persist inline externalMetric (do not set datasource) |
Inspect leftover saved-metric telemetry (already-stored datasource only) |
list_metrics { datasourceId } — not for new reallocations |
| See series keys of a chosen external metric before mapping |
query (type: "externalMetric", groupByFields) |
Workflow A — Create a new virtual dimension on a topic
Use when the user wants a new axis (e.g. "build an Environment dimension", "allocate costs by team").
search with type: ["dimensions"] — discover fields + values: { query: "", type: ["dimensions"] } lists all cos_* field names and top values; repeat with keywords (e.g. "prod", "staging") to find values for conditionCel. Optional: query to understand spend shape.
Tip: Don't hesitate to search extensively at this stage. The goal is to gather all available context for your virtual dimension.
- If you do not know what buckets the dimension should have, ask for expected values first — e.g. Data, ML, Infra.
search with type: ["dimensions"] combines keyword + semantic value discovery. virtual_dimensions hits are name/description keyword search only.
- If building a "Team" dimension, ask the user for specific team names to scan for using search.
- Ask clarifying questions if needed. Batch questions into a single message.
- If multiple mapping approaches exist, present options → user picks a strategy.
- Present proposed rules → explicit rule approval — show the full rule set (or representative summary + edge cases). Do not call
create_virtual_dimension_draft until the user confirms the rules.
create_virtual_dimension_draft — name, optional description, tagNames, rules only (no catch-all — a default leftover rule is added automatically as leftoverRule). Pick the final name now — bqName is derived from this name and is immutable. Rejects invalid payloads — fix errors and retry. On success, draftPersisted: true.
preview_virtual_dimension_draft mode: "costs" — check leftover %
- [if shadowing concerns]
virtual_dimension_overlap_matrix — shadowing between named rules only
update_virtual_dimension_draft — iterate with full rules array (must pass validation to persist)
preview_virtual_dimension_draft mode: "costs" — repeat until satisfied
publish_virtual_dimension — only when the user explicitly confirms
First draft tips: start with 1–5 high-confidence rules; do not put a catch-all in rules; use tagNames (strings), not tag UUIDs.
Workflow B — Iterate on an existing virtual dimension
list_virtual_dimensions with query — find by name and resolve virtualDimensionId (prefer over search for id resolution; use search only for discovery, then list for id)
get — published + draft rules (CEL), dependencies (type: "virtualDimension")
- Optional diagnostics:
preview_virtual_dimension_draft mode: "costs" — per-rule costs + leftover %
preview_virtual_dimension_draft mode: "breakdown" — drill into a rule with ruleId + groupBy
virtual_dimension_overlap_matrix — shadowing / overlap between rules
update_virtual_dimension_draft — full rules array (declarative replace). Copy every existing rule's id from get (draft rules if pending, else published). For dimensionValue rules you may edit name, conditionCel, and allocation.dimensionValue; for other allocation types echo allocation unchanged and edit only name / conditionCel. When reordering, move each id with its rule — ids are sticky. Rejects invalid payloads — nothing is saved until validation passes.
preview_virtual_dimension_draft mode: "costs"
publish_virtual_dimension — only when the user explicitly confirms
If no pending draft exists, update_virtual_dimension_draft seeds one from published state (response may include warning: "Initialized draft from published state").
Workflow C — Reallocate shared cost by a usage metric
Use when the user wants to split shared spend proportionally to a live integration metric (e.g. CPU-hours, requests) — the telemetry allocation type. New reallocations persist inline externalMetric only.
list_metrics { includeExternal: true, search } → pick provider, integrationId, metricName, and a split-by attribute from attributes.
query { type: "externalMetric", provider, integrationId, metricName, aggregator, groupByFields: ["<attribute>"] } → see top series keys. These become mappingParams.mapping keys.
- Present the proposed mapping for explicit rule approval (do not guess bucket labels; map significant values and let the long tail go to leftover).
create_virtual_dimension_draft / update_virtual_dimension_draft with a rule whose allocation is { allocationType: "telemetry", externalMetric: { provider, integrationId, metricName, aggregator, groupByFields }, mappingType: "mapping", mappingParams: { mapping } } (or regexMapping + regexTransformation with named capture groups). Do not set datasource.
preview_virtual_dimension_draft mode: "costs" → confirm the split and leftover share.
publish_virtual_dimension on explicit confirmation.
If get returns a leftover stored { datasource } telemetry rule, echo it unchanged unless the user asks to switch it to an integration.
The iteration loop
After a successful create or update, run preview (BigQuery cost check).
Repeat until leftovers are acceptable or the user is satisfied:
preview_virtual_dimension_draft (mode: "costs") — read summary, totals.leftoverSharePercent, and totals.namedRulesSharePercent
- If leftovers are high →
search / query to find unallocated spend
update_virtual_dimension_draft — add or reorder rules (must pass validation to persist)
- If overlap / shadowing concerns →
virtual_dimension_overlap_matrix
- Go to step 1
Heuristics:
- Leftover > ~10–20%: add rules for the largest unclaimed buckets
- Rule order wrong: put narrower/specific rules before broader ones
- Shadowing: off-diagonal entries in the overlap matrix show spend captured by an earlier rule that also matches a later rule's condition
- Before writing a rule:
preview with mode: "breakdown", ruleId + groupBy to see top values inside a rule's scope
Watch for rule shadowing
After adding a broad rule above a narrower one, a later rule can show 0 in preview because the new rule captures that spend too.
After reordering or adding rules, call virtual_dimension_overlap_matrix — off-diagonal entries show spend captured by an earlier rule that also matches a later rule's condition. If a rule you meant to keep shows 0 in costs preview, check overlap before publishing.
VDIM tool reference
| Tool |
Mutates? |
Use |
list_virtual_dimensions |
no |
Find VDIM by name/tag |
get |
no |
Published + draft rules (CEL), dependencies (type: "virtualDimension") |
create_virtual_dimension_draft |
draft |
New VDIM |
update_virtual_dimension_draft |
draft |
Full rules array replace (or metadata-only when rules omitted) |
preview_virtual_dimension_draft |
no |
BQ: costs or breakdown |
virtual_dimension_overlap_matrix |
no |
BQ: rule overlap / shadowing |
publish_virtual_dimension |
yes |
Promote draft → production + refresh |
Example: "Create an Environment virtual dimension"
search { query: "", type: ["dimensions"] } → find env and its values; { query: "prod", type: ["dimensions"] } → find values like sub_account_id containing "prod"
- Present mapping options → user picks strategy
- Present proposed rules → explicit rule approval
create_virtual_dimension_draft with approved prod/staging/dev rules
preview_virtual_dimension_draft mode: "costs" → leftover 35%
search for remaining unallocated values → update_virtual_dimension_draft with full rules array
preview_virtual_dimension_draft → leftover 4%
- [if needed]
virtual_dimension_overlap_matrix → check for rule shadowing
- User confirms →
publish_virtual_dimension
Safety Rules / Anti-patterns
- Do not publish after preview/overlap when
draftPersisted: false — call update_virtual_dimension_draft (or create_virtual_dimension_draft for a new VDIM)
- Do not publish without
preview when leftovers are still high
- Do not expect create/update to save an invalid draft — they reject invalid payloads (
draftPersisted: false)
- Do not send partial rule arrays to
update_virtual_dimension_draft (always emit full end state when rules is provided)
- Do not include a catch-all / leftover rule in
rules on create or update
- Do not omit
id on rules you intend to keep — always carry forward each existing rule's id
- Do not reorder rules without moving
id with the same logical rule
- Do not use draft-version ids — always use
virtualDimensionId
- Do not put broad rules before narrow ones (causes shadowing)
- Do not call
create_virtual_dimension_draft after strategy-only approval — get explicit rule approval first
- Do not invent VDIM bucket names from spend alone without asking what values the user expects
- Do not call
publish_virtual_dimension before the user explicitly asks to go live
- Do not try to rename the leftover bucket via
update_virtual_dimension_draft — use the Costory web app
- Do not derive
groupBy / filterCel from the display name — always use immutable bqName
- Do not set
datasource / datasourceId on new telemetry reallocations — persist inline externalMetric via list_metrics { includeExternal: true, search }
Related Skills / Next Steps
dashboards — after publish, build charts that groupBy the new bqName
reports — include the new axis in DIGEST hierarchy once computeStatus is COMPLETED
1---2name: virtual-dimensions3description: Use when creating, editing, previewing, or publishing Costory virtual dimensions (custom cost axes) with ordered CEL rules, leftover buckets, overlap/shadowing checks, or telemetry split-by-usage allocations. Call get_skill with skillId "virtual-dimensions" before create_virtual_dimension_draft or update_virtual_dimension_draft.4---56# Virtual Dimensions78A **virtual dimension** is a custom cost axis (e.g. "Environment", "Team", "Product line") built from **ordered rules**. Each rule has:910- **`conditionCel`** — CEL filter selecting rows (e.g. `cos_environment in ["prod"]`)11- **`allocation`** — where matched spend goes. Supports all types: `dimensionValue`, `existingColumn`, `splitCost`, `telemetry` (same shapes as `get` returns for a virtual dimension)1213**Rules are evaluated top-to-bottom; first match wins.** A **leftover rule** catches everything else.1415**Edits happen on a draft.** Publishing is explicit and triggers BigQuery refresh. Production is unchanged until `publish_virtual_dimension`.1617## When to Trigger1819- Building a new custom cost axis (Environment, Team, Product line, …)20- Editing rules on an existing virtual dimension21- Previewing leftover %, breakdown, or rule overlap/shadowing22- Splitting shared cost by a usage metric (`telemetry` allocation)23- Publishing a draft after user confirmation2425## Auth2627Write tools (`create` / `update` / `publish`) are Clerk-only — not available on the service route. Read tools work on both routes.2829## Concepts3031| Concept | Meaning |32|---------|---------|33| Draft vs published | Mutations touch the latest pending draft; published state stays live until publish |34| Draft persistence | `preview_virtual_dimension_draft` and `virtual_dimension_overlap_matrix` (when `includeDraft: true`) return `draftPersisted: true` when a pending draft exists, `draftPersisted: false` when analyzing published-only state in memory (not publishable). `create_virtual_dimension_draft` and `update_virtual_dimension_draft` persist only when validation passes — on success `draftPersisted: true` and `draftValidation: { ok: true }`; on failure nothing is saved (`draftPersisted: false`). `publish_virtual_dimension` requires a **persisted** pending draft — call `update_virtual_dimension_draft` for an existing virtualDimensionId (or `create_virtual_dimension_draft` for a brand-new VDIM) |35| Rule order | Earlier rules claim spend; later overlapping rules are **shadowed** |36| Leftovers | Spend not matched by any named rule — high leftover means add more rules |37| Leftover (MCP) | Auto catch-all returned as `leftoverRule` (not in `rules`). **Do not include a catch-all in `rules` on create or update** — duplicate leftovers cause ordering confusion. There is no leftover-label input on create/update — rename the leftover bucket in the web app |38| Rule ids | **Create:** saveVirtualDimensionDraft re-stamps all rule ids — response ids are authoritative. **Update:** preserves ids from input (including when seeding a draft from published state). **Both:** ids are sticky to logical rules — reorder by moving `id` + `name` + `conditionCel` + `allocation` together; attaching an id to a different condition silently mis-assigns spend |39| Tags | `get` returns `tags` (names) for both published and draft. Pass `tagNames` (strings) on create/update — not tag UUIDs |40| `values` | Output-only — derived from rule allocations; do not send on create/update |41| Allocation shapes | `dimensionValue`: `{ allocationType: "dimensionValue", dimensionValue: "prod" }`; `existingColumn`: `{ allocationType: "existingColumn", existingColumn: "cos_team" }`; `splitCost`: `{ allocationType: "splitCost", reAllocationParams: { type: "custom", partitions: [{ label: "A", weight: 60 }, { label: "B", weight: 40 }] } }` (weights sum to 100); `telemetry`: `{ allocationType: "telemetry", externalMetric: { provider, integrationId, metricName, aggregator, groupByFields: ["<split-by>"] }, mappingType: "mapping" \| "regexMapping" \| "identity", mappingParams: { mapping: { "key": "value" } }, regexTransformation: "..." }` — discover `externalMetric` via `list_metrics` `{ includeExternal: true, search }`. Do **not** set `datasource` on new reallocations (leftover stored rows may still have it — echo unchanged). mapping keys are metric **value** names (not `cos_*` dimensions); unmapped values → leftover. Echo non-`dimensionValue` allocations from get unchanged unless the user explicitly asks to edit advanced allocation behavior |42| CEL only | Always use `conditionCel` strings; never raw query-builder JSON |43| Unlabelled values | Use `cos_environment == null` in `conditionCel` (not `is_null` or string `"null"`) |44| Declarative updates | `update_virtual_dimension_draft` takes the **full desired rules array**, not diffs |45| `name` vs `bqName` | `name` is the display label (can change in drafts). `bqName` is the **immutable** BigQuery / CEL field name (e.g. `virtual_environment`) — set once at `create_virtual_dimension_draft` from the initial name and **never updated**, even after rename or publish. Read `bqName` from create/update/publish/get responses (or `list_virtual_dimensions` when resolving by name only) before `groupBy` / `filterCel`; **never derive it from `name`**. |46| Post-publish query | After publish, use returned `bqName` (not `name`) in `query`. `computeStatus`: `REFRESHING` = refresh job queued — poll `get` or `list_virtual_dimensions` until `COMPLETED` before querying; `TO_REFRESH` = draft promoted but refresh job queuing failed — retry `publish_virtual_dimension` or poll `computeStatus` until `COMPLETED` |4748## Strategy approval ≠ rule approval4950When several mapping approaches exist (e.g. reuse `env` vs derive from `cos_sub_account_id` patterns vs split by `cos_team`), **present the options and let the user pick a strategy first**.5152**Picking a strategy is not approval to create the draft.** After the user chooses an approach:53541. **Draft the concrete rules** for that strategy — each rule's `name`, `conditionCel`, and `allocation` (leftover catch-all is added automatically; do not put it in `rules`).552. **Present the full proposed rule set** — or, when there are many rules, a representative summary (rule order, main buckets, example CEL per bucket) **plus called-out edge cases** (null/unlabelled values, ambiguous values, known shadowing risks, spend you could not classify).563. **Wait for explicit rule approval** — the user must confirm the rules (or request edits) before you call `create_virtual_dimension_draft`.5758Do not call `create_virtual_dimension_draft` immediately after the user says "use approach B" or "map by sub-account" — that only approves the **mapping strategy**, not the **rule set**.5960## When target values are unclear6162If you do not know what buckets the finished dimension should have — vague ask ("organize our cloud spend"), no obvious `cos_*` field, or search results do not suggest a sensible split — **ask the user for the values they expect** before proposing mapping strategies or rules.6364Example prompt: *"What values should this dimension use? For example: Data, ML, Infra — or prod, staging, dev?"*6566Use their answer as:6768- Target `allocation.dimensionValue` labels (one rule per bucket, unless they specify otherwise)69- Keywords for `search` and `query` when drafting `conditionCel`70- A checklist when presenting the proposed rule set (did every expected value get a rule? what lands in leftover?)7172Do not invent bucket names from spend patterns alone without confirming — inferred labels often miss how the org actually thinks about costs. Batch value questions with other clarifying questions in a single message when possible.7374## Supporting tools (before VDIM tools)7576| When | Tool |77|------|------|78| Discover valid `cos_*` dimension names | `search` with `type: ["dimensions"]` (empty query lists all) |79| Find dimension values / existing VDIMs for a topic | `search` with `type: ["dimensions", "virtual_dimensions"]` |80| Explore spend to decide what rules to write | `query` |81| Pick a natural split dimension for unclaimed spend | `suggest_groupby` |82| Discover a live integration metric for a new `telemetry` allocation | `list_metrics` `{ includeExternal: true, search }` — persist inline `externalMetric` (do not set `datasource`) |83| Inspect leftover saved-metric telemetry (already-stored `datasource` only) | `list_metrics` `{ datasourceId }` — not for new reallocations |84| See series keys of a chosen external metric before mapping | `query` (`type: "externalMetric"`, `groupByFields`) |8586## Workflow A — Create a new virtual dimension on a topic8788Use when the user wants a **new** axis (e.g. "build an Environment dimension", "allocate costs by team").89901. `search` with `type: ["dimensions"]` — discover fields + values: `{ query: "", type: ["dimensions"] }` lists all `cos_*` field names and top values; repeat with keywords (e.g. `"prod"`, `"staging"`) to find values for `conditionCel`. Optional: `query` to understand spend shape.9192> **Tip:** Don't hesitate to search extensively at this stage. The goal is to gather all available context for your virtual dimension.93> - If you do not know what buckets the dimension should have, **ask for expected values first** — e.g. Data, ML, Infra.94> - `search` with `type: ["dimensions"]` combines keyword + semantic value discovery. `virtual_dimensions` hits are name/description keyword search only.95> - If building a "Team" dimension, ask the user for specific team names to scan for using search.96> - Ask clarifying questions if needed. Batch questions into a single message.97982. **If multiple mapping approaches exist**, present options → user picks a **strategy**.993. **Present proposed rules → explicit rule approval** — show the full rule set (or representative summary + edge cases). **Do not** call `create_virtual_dimension_draft` until the user confirms the rules.1004. `create_virtual_dimension_draft` — `name`, optional `description`, `tagNames`, `rules` only (no catch-all — a default leftover rule is added automatically as `leftoverRule`). **Pick the final name now** — `bqName` is derived from this name and is immutable. Rejects invalid payloads — fix errors and retry. On success, `draftPersisted: true`.1015. `preview_virtual_dimension_draft` `mode: "costs"` — check leftover %1026. **[if shadowing concerns]** `virtual_dimension_overlap_matrix` — shadowing between named rules only1037. `update_virtual_dimension_draft` — iterate with full `rules` array (must pass validation to persist)1048. `preview_virtual_dimension_draft` `mode: "costs"` — repeat until satisfied1059. `publish_virtual_dimension` — only when the user explicitly confirms106107**First draft tips:** start with 1–5 high-confidence rules; do not put a catch-all in `rules`; use `tagNames` (strings), not tag UUIDs.108109## Workflow B — Iterate on an existing virtual dimension1101111. `list_virtual_dimensions` with `query` — find by name and resolve `virtualDimensionId` (prefer over `search` for id resolution; use `search` only for discovery, then `list` for id)1122. `get` — published + draft rules (CEL), dependencies (`type: "virtualDimension"`)1133. Optional diagnostics:114 - `preview_virtual_dimension_draft` `mode: "costs"` — per-rule costs + leftover %115 - `preview_virtual_dimension_draft` `mode: "breakdown"` — drill into a rule with `ruleId` + `groupBy`116 - `virtual_dimension_overlap_matrix` — shadowing / overlap between rules1174. `update_virtual_dimension_draft` — full `rules` array (declarative replace). Copy every existing rule's `id` from `get` (draft rules if pending, else published). For `dimensionValue` rules you may edit `name`, `conditionCel`, and `allocation.dimensionValue`; for other allocation types echo `allocation` unchanged and edit only `name` / `conditionCel`. When reordering, move each `id` with its rule — ids are sticky. Rejects invalid payloads — nothing is saved until validation passes.1185. `preview_virtual_dimension_draft` `mode: "costs"`1196. `publish_virtual_dimension` — only when the user explicitly confirms120121If no pending draft exists, `update_virtual_dimension_draft` seeds one from published state (response may include `warning: "Initialized draft from published state"`).122123## Workflow C — Reallocate shared cost by a usage metric124125Use when the user wants to split shared spend proportionally to a live integration metric (e.g. CPU-hours, requests) — the `telemetry` allocation type. New reallocations persist inline `externalMetric` only.1261271. `list_metrics` `{ includeExternal: true, search }` → pick `provider`, `integrationId`, `metricName`, and a split-by attribute from `attributes`.1282. `query` `{ type: "externalMetric", provider, integrationId, metricName, aggregator, groupByFields: ["<attribute>"] }` → see top series keys. These become `mappingParams.mapping` keys.1293. Present the proposed mapping for **explicit rule approval** (do not guess bucket labels; map significant values and let the long tail go to leftover).1304. `create_virtual_dimension_draft` / `update_virtual_dimension_draft` with a rule whose `allocation` is `{ allocationType: "telemetry", externalMetric: { provider, integrationId, metricName, aggregator, groupByFields }, mappingType: "mapping", mappingParams: { mapping } }` (or `regexMapping` + `regexTransformation` with named capture groups). Do not set `datasource`.1315. `preview_virtual_dimension_draft` `mode: "costs"` → confirm the split and leftover share.1326. `publish_virtual_dimension` on explicit confirmation.133134If `get` returns a leftover stored `{ datasource }` telemetry rule, echo it unchanged unless the user asks to switch it to an integration.135136## The iteration loop137138After a successful create or update, run **preview** (BigQuery cost check).139140Repeat until leftovers are acceptable or the user is satisfied:1411421. `preview_virtual_dimension_draft` (`mode: "costs"`) — read `summary`, `totals.leftoverSharePercent`, and `totals.namedRulesSharePercent`1432. If leftovers are high → `search` / `query` to find unallocated spend1443. `update_virtual_dimension_draft` — add or reorder rules (must pass validation to persist)1454. If overlap / shadowing concerns → `virtual_dimension_overlap_matrix`1465. Go to step 1147148**Heuristics:**149150- **Leftover > ~10–20%:** add rules for the largest unclaimed buckets151- **Rule order wrong:** put narrower/specific rules **before** broader ones152- **Shadowing:** off-diagonal entries in the overlap matrix show spend captured by an earlier rule that also matches a later rule's condition153- **Before writing a rule:** `preview` with `mode: "breakdown"`, `ruleId` + `groupBy` to see top values inside a rule's scope154155## Watch for rule shadowing156157After adding a broad rule above a narrower one, a later rule can show **0** in preview because the new rule captures that spend too.158159**After reordering or adding rules**, call `virtual_dimension_overlap_matrix` — off-diagonal entries show spend captured by an earlier rule that also matches a later rule's condition. If a rule you meant to keep shows 0 in costs preview, check overlap before publishing.160161## VDIM tool reference162163| Tool | Mutates? | Use |164|------|----------|-----|165| `list_virtual_dimensions` | no | Find VDIM by name/tag |166| `get` | no | Published + draft rules (CEL), dependencies (`type: "virtualDimension"`) |167| `create_virtual_dimension_draft` | draft | New VDIM |168| `update_virtual_dimension_draft` | draft | Full rules array replace (or metadata-only when `rules` omitted) |169| `preview_virtual_dimension_draft` | no | BQ: costs or breakdown |170| `virtual_dimension_overlap_matrix` | no | BQ: rule overlap / shadowing |171| `publish_virtual_dimension` | **yes** | Promote draft → production + refresh |172173## Example: "Create an Environment virtual dimension"1741751. `search` `{ query: "", type: ["dimensions"] }` → find `env` and its values; `{ query: "prod", type: ["dimensions"] }` → find values like `sub_account_id` containing "prod"1762. **Present mapping options** → user picks strategy1773. **Present proposed rules → explicit rule approval**1784. `create_virtual_dimension_draft` with approved prod/staging/dev rules1795. `preview_virtual_dimension_draft` `mode: "costs"` → leftover 35%1806. `search` for remaining unallocated values → `update_virtual_dimension_draft` with full rules array1817. `preview_virtual_dimension_draft` → leftover 4%1828. **[if needed]** `virtual_dimension_overlap_matrix` → check for rule shadowing1839. User confirms → `publish_virtual_dimension`184185## Safety Rules / Anti-patterns186187- Do not publish after preview/overlap when `draftPersisted: false` — call `update_virtual_dimension_draft` (or `create_virtual_dimension_draft` for a new VDIM)188- Do not publish without `preview` when leftovers are still high189- Do not expect create/update to save an invalid draft — they reject invalid payloads (`draftPersisted: false`)190- Do not send partial rule arrays to `update_virtual_dimension_draft` (always emit full end state when `rules` is provided)191- Do not include a catch-all / leftover rule in `rules` on create or update192- Do not omit `id` on rules you intend to keep — always carry forward each existing rule's `id`193- Do not reorder rules without moving `id` with the same logical rule194- Do not use draft-version ids — always use `virtualDimensionId`195- Do not put broad rules before narrow ones (causes shadowing)196- Do not call `create_virtual_dimension_draft` after strategy-only approval — get **explicit rule approval** first197- Do not invent VDIM bucket names from spend alone without asking what values the user expects198- Do not call `publish_virtual_dimension` before the user explicitly asks to go live199- Do not try to rename the leftover bucket via `update_virtual_dimension_draft` — use the Costory web app200- Do not derive `groupBy` / `filterCel` from the display `name` — always use immutable `bqName`201- Do not set `datasource` / `datasourceId` on new telemetry reallocations — persist inline `externalMetric` via `list_metrics` `{ includeExternal: true, search }`202203## Related Skills / Next Steps204205- `dashboards` — after publish, build charts that `groupBy` the new `bqName`206- `reports` — include the new axis in DIGEST hierarchy once `computeStatus` is `COMPLETED`