Migration on-ramp: assess, copy in, translate¶
Surface:
/admin/migrate— the Assess, Copy in and Translate tabs Backend: theloom-migrateestate-reader Container App (internal ingress), a real Azure Data Factory Copy pipeline, and in-boundary transpilers reusing Loom's own DAX parser and code-report parser Kill-switch flags:n-m1-estate-assess,n-m2-copy-in,n-m3-translate(all default ON) Honest gate:LOOM_MIGRATE_URL(gate idsvc-loom-migrate), plus a per-connector connection prerequisite
Bringing an existing estate in. Point Loom at a Snowflake, Databricks Unity Catalog, Microsoft Fabric or Power BI estate; get a readiness report; land the data; translate the code. Three steps, three tabs, one shared report shape.
A Fabric or Power BI estate is only ever a migration source here. Loom itself has no Fabric dependency, and the default path reaches no Fabric host.
Why it exists¶
"Migrate to our platform" usually means a consulting engagement and a spreadsheet. The on-ramp replaces the spreadsheet with a real enumeration: what objects exist, which map one-to-one onto a Loom item type, and which genuinely need a human. The honesty rule is the whole value — an object kind with no confident target resolves to needs-review with a reason, never a fabricated one-to-one.
Step 1 — Assess (M1)¶
- Open Admin → Migrate → Assess.
- Pick the source type: Snowflake, Databricks Unity Catalog, Microsoft Fabric, or Power BI.
- Supply the connection — account or workspace URL, the catalog or workspace id, and a token stored as a Key Vault secret reference.
- Run the assessment. The
loom-migratereader enumerates the estate; the assessment engine maps each enumerated object onto a Loom item type (lakehouse,warehouse,semantic-model,report,notebook,data-pipeline,kql-database,eventhouse,eventstream,mirrored-database,dataflow,paginated-report,dashboard,ml-model, and so on). - Read the readiness report. Per object: the source kind, the target Loom item type, a 1:1 or needs-review effort flag, and a human-readable reason.
The engine never invents inventory — it maps only what the reader actually enumerated.
Step 2 — Copy in (M2)¶
- Switch to the Copy in tab. The copy plan is built from the M1 readiness report, so you are landing exactly what was assessed.
- Review and adjust the plan — which assessed tables to land.
- Run it. Each table lands in ADLS Bronze via a real Azure Data Factory Copy pipeline — the mirroring substrate, in reverse.
- Watch the live monitor. Per-object status through the copy job.
- Optionally materialize managed Delta in the target Loom lakehouse, so the landed data is immediately queryable as a first-class table.
ADF runs in-boundary. Nothing about copy-in requires a Fabric capacity.
Step 3 — Translate (M3)¶
- Switch to the Translate tab and choose what to transpile:
| Kind | Source | Lands as |
|---|---|---|
| SQL view | Snowflake or T-SQL / Fabric | Loom SQL |
| Stored procedure / UDF | Snowflake or T-SQL / Fabric | Loom SQL |
| DAX measure | Power BI / Fabric | a semantic-contract measure (and optionally into the semantic contract directly) |
| Report | Power BI / Fabric | a Loom code-report |
- Paste or select the source and pick the dialect.
- Review the side-by-side diff. Source on the left, generated artifact on the right, with per-construct supported / needs-review badges. An unsupported construct is shown needs-review with the exact reason — never a fabricated translation.
- Accept. A supported artifact lands as a draft Loom item through the normal item-create path, so nothing goes live without a human opening it.
- Run the translated asset and compare against the source system.
The transpilers reuse machinery Loom already ships and tests: the DAX parser and SQL-fold engine behind the semantic model's DAX query view, and the code-report parser. They run fully in-boundary — pure parse and fold, no SaaS call — so translation works in a disconnected enclave.
What the backend actually does¶
| Control | Backend |
|---|---|
| Assess | POST /api/migrate/assess -> the internal-ingress loom-migrate reader, then the pure assessment engine |
| Copy in | POST /api/migrate/copy -> a real ADF Copy pipeline into ADLS Bronze, then optional managed-Delta materialization |
| Translate | POST /api/migrate/translate -> the in-boundary transpilers |
| Audit | Every enumeration is an audited BFF call; the reader is never public |
Deployment (default ON) and honest gates¶
LOOM_MIGRATE_URL is set by the deployment itself. Since 2026-07-28 admin-plane/main.bicep deploys platform/fiab/bicep/modules/data-plane/loom-migrate-aca.bicep on every apps-enabled deploy, in every Container Apps boundary — Commercial, GCC, GCC-High and IL5 — and wires https://<fqdn> onto the Console, so a fresh push-button deploy closes the svc-loom-migrate gate with no operator step. The reader runs minReplicas: 0, so default-ON costs approximately $0 at idle: it only spins up during an assessment, where a cold start is seconds against an action that already takes minutes. Admins opt OUT with observabilityConfig.backendOverrides.loomMigrate = 'disabled'.
LOOM_MIGRATE_URLunset. Only possible on a pre-2026-07-28 estate that has not been redeployed, before the apps tier deploys, or after an explicit opt-out./admin/migratestill renders; Assess honest-gates with a Fix-it naming the variable and the bicep module (platform/fiab/bicep/modules/data-plane/loom-migrate-aca.bicep). No fabricated counts, ever.- A connector without its connection prerequisite. The reader replies with a structured connector gate, and the surface renders a precise message bar naming what is missing (account or workspace URL, token) rather than showing zero.
- In an IL5 boundary the reader itself runs in-boundary and the on-ramp works disconnected; individual SaaS-source connectors stay honestly gated until their connection prerequisite is provided.
Getting the image into a sovereign ACR (GCC-High / IL5)¶
The template deploys the reader in Gov exactly as it does in Commercial, so the only Gov prerequisite is the one loom-console already has: the loom-migrate image must be in that boundary's registry at the tag appImageTags.loomMigrate resolves to (v0.1 by default in params/gcc-high.bicepparam and params/il5.bicepparam). A missing manifest fails the Container App PUT with MANIFEST_UNKNOWN.
Build it server-side — the only mechanism that reaches a publicNetworkAccess=Disabled registry — with either .github/workflows/build-fiab-images-acr-tasks.yml (boundary=GCC-High / IL5, full image set) or .github/workflows/gov-provision-streaming-migrate.yml (mode=build-only for just loom-migrate + loom-risingwave; mode=build-and-deploy also stands both Container Apps up and wires the vars on an estate that is already running). Not build-fiab-images.yml: it uses the Commercial service principal and pushes client-side, so it cannot produce a Gov image, and it now hard-fails when dispatched with a Gov boundary.
Kill-switches¶
| Flag | OFF behaviour |
|---|---|
n-m1-estate-assess | /api/migrate/assess returns a guided "turned off" 503 and the surface's function is hidden on the next load. Nothing else is affected. |
n-m2-copy-in | /api/migrate/copy returns a guided 503 and the tab's function is hidden. Assessment, in-flight pipelines, and every other surface are unaffected. |
n-m3-translate | POST /api/migrate/translate returns a guided 503 and the Translate tab is hidden. Nothing else is affected. |
All three are default ON and need no roll to flip.
Related¶
- Workspace portability — moving workspaces within Loom
- Forward migration to Microsoft Fabric — the outbound direction
- Upgrade and migration
- Tutorial — Forward-migrate a Lakehouse to Fabric