powerbi-to-sl
Use this skill to migrate an existing Microsoft Power BI semantic model into a Keboola semantic layer model. Accepts two input shapes: (1) TMDL folder extracted with the Microsoft Power BI Modeling MCP server in `--readonly` mode (canonical, captures DAX + Power Query M expressions verbatim); (2) per-table JSON produced by older `get_semantic_model` calls (missing M steps). Maps Power BI tables → Keboola semantic-dataset, measures → semantic-metric (DAX preserved verbatim, flagged for SQL translation), relationships → semantic-relationship. Produces JSON files ready to push to the Keboola metastore via `sl-toolkit` or the REST API. Use for: migrate PowerBI semantic model, convert Power BI to Keboola SL, translate TMDL, import PowerBI tables, powerbi-to-sl, pbi-migration, brownfield semantic layer. Trigger when the user has PowerBI artifacts (TMDL folder, `*_semantic_layer.json`, `.bim`, `.pbip`) and wants them as a Keboola semantic layer.
How do I install this agent skill?
npx skills add https://github.com/keboola/ai-kit --skill powerbi-to-slIs this agent skill safe to install?
- Gen Agent Trust Hubpass
This skill facilitates the migration of Microsoft Power BI semantic models to the Keboola platform by converting TMDL or JSON artifacts into Keboola semantic layer configurations. It involves local file processing, structural mapping, and optional deployment to the Keboola Metastore API.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
What does this agent skill do?
powerbi-to-sl — Power BI → Keboola Semantic Layer Migration
Read existing PowerBI artifacts → map to Keboola SL payloads → write JSON
files → user confirms → push via sl-toolkit or direct REST. Companion to
sl-toolkit (greenfield); this is the brownfield path.
Step 0 — Identify input
Ask the user where the PowerBI artifacts are, then auto-detect format:
INPUT_DIR="$(read -p 'Path to PowerBI artifacts: ' x; echo "$x")"
python3 -c "
import os, sys
p = sys.argv[1]
tmdl = [f for f in os.listdir(p) if f.endswith('.tmdl')] if os.path.isdir(p) else []
jsons = [f for f in os.listdir(p) if f.endswith('_semantic_layer.json')] if os.path.isdir(p) else []
if tmdl: print('tmdl')
elif jsons: print('per-table-json')
else: print('unknown')
" "$INPUT_DIR"
If unknown, ask the user to clarify, or have them run Microsoft's Power BI
Modeling MCP with --readonly against their dataset to produce TMDL first.
Step 1 — Business questions (before mapping)
"Three quick questions:
- What is the Keboola bucket prefix for these tables? (e.g.,
in.c-pbi-migration— used to buildtableIdfor each semantic-dataset)- Semantic model name + display name? (e.g.,
acme-pbi-migration/ "Acme Power BI Migration")- Snowflake or BigQuery dialect? (default: Snowflake)"
Persist answers as BUCKET_PREFIX, MODEL_NAME, MODEL_DISPLAY, DIALECT.
Step 2 — Run the migrator
python3 scripts/migrate.py \
--input "$INPUT_DIR" \
--input-format "$FORMAT" \
--bucket-prefix "$BUCKET_PREFIX" \
--model-name "$MODEL_NAME" \
--model-display "$MODEL_DISPLAY" \
--dialect "$DIALECT" \
--output ./out/
Output layout:
out/
semantic-model.json
semantic-dataset/
<bucket-prefix>.<table>.json # one per PowerBI table
semantic-metric/
<measure-slug>.json # one per PowerBI measure
semantic-relationship/
<from>__<to>.json # one per PowerBI relationship
WARNINGS.md # DAX-not-translated, unmapped types, etc.
Step 3 — Review WARNINGS.md with the user
The migrator preserves DAX measure expressions verbatim in
semantic-metric.sql. Show the user WARNINGS.md and ask whether to:
- Punt — leave DAX in place; downstream Kai (or a human) rewrites as SQL on-demand.
- Auto-translate trivial cases —
SUM('Table'[Col])→SUM("Table"."Col"),COUNT,DISTINCTCOUNT,AVERAGE. Anything withCALCULATE, filter context,RELATED, time-intelligence, or variables stays flagged. - Human pass — open the WARNINGS list and rewrite line by line.
Default: punt + show the list. Trivial-case translation is opt-in.
Step 4 — Validate against Keboola SL schemas (jsonschema)
python3 -c "
import json, jsonschema, glob, sys, os
schemas_dir = os.environ['SCHEMAS_DIR']
schemas = {
name: json.load(open(f'{schemas_dir}/semantic-{name}_schema_1.0.0.json'))
for name in ['model','dataset','metric','relationship']
}
errs = 0
for kind, schema in schemas.items():
pattern = './out/semantic-model.json' if kind == 'model' else f'./out/semantic-{kind}/*.json'
for f in glob.glob(pattern):
try:
jsonschema.validate(json.load(open(f)), schema)
except jsonschema.ValidationError as e:
errs += 1
print(f'❌ {f}: {e.message}')
print(f'{errs} validation errors' if errs else '✅ all valid')
"
The same validation runs as a pytest assertion in tests/test_smoke.py. Set
SCHEMAS_DIR to the directory containing the Keboola metastore semantic-*
JSON schemas.
Step 5 — Push (optional, gated on user confirmation)
Confirm with the user before any write. Two paths:
Path A — via sl-toolkit (preferred if that plugin is installed; it
owns the canonical generate–validate–push pipeline):
"Hand off to
sl-toolkit? It owns the canonical push pipeline for any SL model. Or push directly to the metastore REST API?"
If sl-toolkit chosen, invoke its push step with --source ./out/.
Path B — direct REST (when sl-toolkit is not available):
import json, os, urllib.request, glob
from concurrent.futures import ThreadPoolExecutor
TOKEN = os.environ['KBC_STORAGE_TOKEN']
METASTORE = os.environ['KBC_METASTORE_URL'] # e.g. https://metastore.us-east4.gcp.keboola.com
def post(kind, payload):
req = urllib.request.Request(
f'{METASTORE}/api/v1/repository/semantic-{kind}',
data=json.dumps(payload).encode(),
headers={'X-StorageAPI-Token': TOKEN, 'Content-Type': 'application/json'},
method='POST')
return urllib.request.urlopen(req, timeout=30).read()
# Model first — get the UUID, propagate to children
model_uuid = json.loads(post('model', json.load(open('./out/semantic-model.json'))))['data']['id']
for kind in ['dataset','metric','relationship']:
payloads = []
for f in glob.glob(f'./out/semantic-{kind}/*.json'):
p = json.load(open(f))
p['modelUUID'] = model_uuid
payloads.append(p)
with ThreadPoolExecutor(max_workers=10) as ex:
list(ex.map(lambda p: post(kind, p), payloads))
Mapping reference
| Power BI | Keboola SL |
|---|---|
tables[].name | semantic-dataset.name + tableId |
tables[].description | semantic-dataset.description |
tables[].columns[].name | semantic-dataset.fields[].name |
tables[].columns[].dataType | semantic-dataset.fields[].type * |
tables[].columns[].description | semantic-dataset.fields[].description |
| PK column hints | semantic-dataset.primaryKey |
tables[].measures[].expression (DAX) | semantic-metric.sql (verbatim) |
tables[].measures[].description | semantic-metric.description |
relationships[] | semantic-relationship |
Type mapping: string→string, int64/integer→integer,
double/decimal/currency→decimal, dateTime/date→datetime/date,
boolean→boolean. Unknown types fall back to string and get logged.
Known gotchas
- Per-table JSON loses Power Query M steps. The older
get_semantic_modeloutput does not capture M expressions. If full fidelity is required, re-extract with Microsoft's Power BI Modeling MCP--readonlyflag to produce TMDL, which captures DAX and M natively. - DAX ≠ SQL. Verbatim preservation is the default; downstream Kai or a
human translates.
CALCULATE,RELATED, time-intelligence, and variables do not survive trivial translation. - PowerBI table names contain spaces and non-ASCII characters. Slugify
for filenames; keep originals in
name/displayName. UTF-8 paths are fine on macOS/Linux but watch for filesystem-quirky environments. - PK detection is best-effort. Power BI doesn't always mark PKs
explicitly; the migrator uses (a) explicit
isKeymarkers in TMDL, (b) relationshiptocolumns as a heuristic, (c) falls back to empty primaryKey + a WARNINGS entry asking the user to pick. - Relationship cardinality.
manyToOneandoneToOne→inner;oneToMany→leftfrom the "many" side.manyToManyproduces a WARNINGS entry — the SL model expects star-schema friendliness.
Why two skills exist (powerbi-to-sl vs. sl-toolkit)
| Aspect | sl-toolkit | powerbi-to-sl (this) |
|---|---|---|
| Direction | Greenfield (Keboola tables → SL) | Brownfield (Power BI → SL) |
| Input | Keboola storage + SQL transformations | TMDL folder or per-table JSON |
| Generation | LLM-driven from schema + KPIs | Mechanical structural translation |
| Validation | Auto-fix + LLM critique | jsonschema validate + WARNINGS |
| Push | Owns push pipeline | Hands off to sl-toolkit push |
When both are installed, this skill produces SL payloads and delegates
push to sl-toolkit. Reuse, not overlap.
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/keboola/ai-kit/powerbi-to-sl">View powerbi-to-sl on skillZs</a>