Lost Connections - How item references survive deployment in Microsoft Fabric
When autobinding, variable libraries, and fabric-cicd can rebind your item references between environments — and when nothing can
A notebook and a lakehouse meet in a Dev workspace. Things go well. Queries flow, the two are inseparable, everything just clicks.
Then the solution is promoted to Test. Both of them make the move — but the notebook still has the old number saved. It keeps calling the lakehouse back in Dev: quietly reading the wrong data if it has permission, failing outright if it doesn't. The lakehouse standing right beside it in Test with the same name and definition, never hears a thing.
This post discusses which references Fabric rebinds for you, which ones you have to manage yourself, which tool to reach for in each case, and which references nothing in the platform can carry across environments today.
The mechanic underneath¶
Every heartbreak here has the same cause. When one Fabric item references another, that reference is stored inside the item's definition, as an identifier. There are two kinds:
- Logical ID — a portable identifier generated by Fabric and stored in each item's
.platformfile. Items with the same logical ID are treated as the same item across workspaces - Object ID — a workspace-specific GUID that identifies one particular instance, in one particular workspace
When Fabric exports an item to git, it swaps some object IDs for logical IDs. Sync that branch into another workspace and those logical IDs resolve against the target's items. Everything that wasn't swapped stays a raw GUID, still pointing at the workspace you came from.
So every mechanism below is just a different answer to one question: who rewrites the IDs, and when?
Git Auto-Binding¶
Who rewrites the IDs? Fabric does, during update-from-git, by resolving logical IDs against the target workspace.
This is the honeymoon phase, and when it works, it just works — no configuration, no rules, no parameter files. Sync a branch into a new workspace and the pipeline finds its notebook, the notebook finds its environment, the shortcut finds its lakehouse.
Autobinding is same-workspace only
Dependency binding applies to references between items in the same workspace. If an item references a Fabric item in a different workspace, that reference is an object ID and never binds.
Where it works¶
Many same-workspace item pairs store logical IDs and rebind on their own; an internal OneLake shortcut finds its lakehouse or warehouse. Some come with strings attached: notebook → lakehouse only binds if you turn on the "Lakehouse Auto-Binding in Git" setting, per notebook, off by default.
A few more resolve by name or path rather than by ID — a Power BI report reaches its semantic model through a relative byPath reference, and a Dataflow Gen2 source rebinds only if authored as a relative reference. These work as long as naming and layout are identical across environments, which makes them quietly fragile.
Where it doesn't¶
The references left over are object IDs that are not rewritten to logical IDs:
- Semantic model → SQL analytics endpoint — the Direct Lake connection string holds a workspace-specific URL and GUID, so every Direct Lake model stays pointed at the source workspace
- Dataflow Gen2 destinations — all of them, every destination type
- Spark Job Definition → lakehouse — the
defaultLakehouseArtifactIdis an object ID
The full item-by-item picture lives in Microsoft's dependency binding matrix. Check your specific item pairs there, because the pattern is inconsistent enough that guessing is dangerous (a pipeline's notebook reference binds; its Spark Job Definition reference doesn't).
Use autobinding when
Your solution lives in one workspace per environment, promotion is a git sync, and your references all auto-bind per the matrix. Turn on Lakehouse Auto-Binding in each notebook, author Dataflow Gen2 sources as relative references, and keep item names consistent so the by-name references behave. Anything the matrix says doesn't bind needs Variable Libraries or Fabric-cicd.
Variable Libraries¶
Who rewrites the IDs? Nobody rewrites them. The consuming item asks a variable library for the value at run time, and each workspace has its own active value set.
What they're genuinely the right answer for¶
Anything that is configuration rather than identity — a wait duration, a source path, a row limit that should be small in Test, a feature flag — consumed by one of the supported item types. This is the use case they were built for and they do it well.
For connections, they're the mechanism Microsoft points you at (connections never autobind, and the binding matrix's own advice is "use variable libraries with environment-specific value sets") — but read the consumer list below before planning around it, because the typed connection reference is narrower than that advice suggests.
The coverage you actually get¶
Support comes in three tiers, and each is narrower than the last:
- Basic variables have a some supported items. These include: pipelines, notebooks, shortcuts, Dataflow Gen2, copy jobs. But, semantic models can't consume variables at all, and neither can reports or Spark Job Definitions
-
ItemReferencehold a reference to an existing Fabric item. They are only supported by three consuming items -
ConnectionReferencehold a reference to an external data connection (For example, Snowflake, Azure SQL). They are only supported by two consuming items
Check the linked supported-items lists before designing around any of these — they're short, they differ per variable type, and each consuming item integrates variables differently.
The trust issues¶
- Chicken-and-egg. An
ItemReferencestores aworkspaceId+itemIdpair, which means the target item must already exist before you can record its address. If you want to deploy into a fresh environment, you have no idea what the GUID will be, so you can't pre-author a value set. You can only create it after the target is published - Both advanced types are static. An item reference "points to a specific item and not automatically adjusting across environments"; connection references likewise "don't auto-bind during deployment — their values remain fixed across environments." Value sets let you hold a different target per stage. You still maintain those GUIDs by hand
- Activation is a separate step. There is exactly one active value set per workspace at a time, this is a workspace-level setting and is not tracked by git. Git only carries the value set definitions, this is by design. This means a deploy doesn't overwrite it, and syncing Dev's commit into Test won't flip Test off its "test" set, but it means a new workspace has the Default set active, requiring manual intervention
- The library must live in the consuming item's workspace. A shared configuration workspace isn't expressible
- No secrets. Values are plain text in git
Use variable libraries when
You need per-environment configuration consumed by one of the several supported item types, and you want it managed centrally in one reviewable item rather than scattered across item definitions.
Reach for the advanced types only within their real reach: ConnectionReference for notebooks and user data functions, ItemReference for shortcuts, user data functions and NotebookUtils notebooks — and in both cases only where the target already exists in every environment you deploy to. Everywhere else, use fabric-cicd.
fabric-cicd¶
Who rewrites the IDs? fabric-cicd does, on your machine or your build agent, after reading the definition from the repo and before deploying it to the workspace.
This is the piece git sync is missing: a transform step. publish_all_items() pushes definitions from the repo, and parameter.yml rewrites them on the way through. Because it operates on the raw definition text, it can be used for any item that is supported by CICD.
The simplest form is a literal swap per environment:
find_replace:
# dev's committed lakehouse GUID → the right one per environment
- find_value: "81bbb339-8d0b-46e8-bfa6-289a159c0733"
replace_value:
TEST: "5d6a1b16-447f-464a-b959-45d0fed35ca4"
PROD: "9b7c341f-408d-48f1-9354-5f6d5b3d06a1"
That works, but it's a hand-maintained GUID list, which is no better than variable libraries. The diamond is dynamic replacement: values resolved at deploy time against the workspace being deployed to.
find_replace:
# whatever this environment's own lakehouse is
- find_value: "81bbb339-8d0b-46e8-bfa6-289a159c0733"
replace_value:
_ALL_: "$items.Lakehouse.Sales.$id"
# Direct Lake models need the SQL endpoint, not the item ID
- find_value: "xyz123abc.datawarehouse.fabric.microsoft.com"
replace_value:
_ALL_: "$items.Lakehouse.Sales.$sqlendpoint"
$workspace.$id resolves to whichever workspace this deploy targets. $items.<Type>.<Name>.$id looks the item up in that workspace at publish time, with $sqlendpoint, $sqlendpointid and $queryserviceuri available for the connection-string cases. One rule, no per-environment literals, and it resolves correctly even on a first deploy into a workspace that was empty ten seconds ago.
The sharp edges¶
There are some gotchas to watch for:
-
is_regexand dynamic variables cannot be combined. A rule is either a regex match or it uses$…variables — never both. If you were hoping to write "any GUID in this position becomes the target's lakehouse", you can't; thefind_valuehas to be a literal -
find_valuebarely supports dynamic variables. Only$workspace.*variables are allowed there, and$itemsnotation is rejected outright — those values don't exist in the source files - Non-matches fail silently. A
find_valuethat no longer matches find nothing, replaces nothing and reports nothing - Dynamic variables trigger eager SQL endpoint resolution. Use any dynamic variable — even one that never mentions
$sqlendpoint— and endpoints resolve for every lakehouse, mirrored database, warehouse and SQL database in the target workspace. If any of them is still provisioning, "the deployment fails before any item is published". This might mean you need to split your deploy into two calls: first the lakehouses, then the rest - Dynamic variables disable bulk publish, falling back to standard publishing
-
_ALL_must be the only environment key in areplace_valuewhen you use it
Use fabric-cicd when
Any reference doesn't autobind and the consuming item can't read a variable library — which is most of them. It's the only option for semantic models (Direct Lake connection strings, and semantic models can't consume variables at all), Dataflow Gen2 destinations, and Spark Job Definitions. Prefer dynamic $items… variables over literal per-environment GUIDs wherever the item is in the workspace you're deploying to.
What nothing covers¶
We talked about the "happy path", deploying a single workspace per environment. Autobinding handles a large slice, and dynamic parameter.yml variables cleanly handle the rest. The gaps that remain are all on the other side of a workspace boundary.
Cross-workspace bindings can't follow the environment¶
Real platforms are rarely one workspace per environment. Let's say you have a data workspace and a reporting workspace per environment; so we can define different permissions or configurations per workspace. We could then have a Direct Lake model in the reporting workspace reading SQL analytics endpoint in the data workspace.
Git autobinding is out by definition. Deployment pipelines do have their own cross-workspace autobinding — but it only pairs items sitting at the same stage index across pipelines with equal stage counts, it follows Power BI-shaped dependencies like report → semantic model, and a Direct Lake model still doesn't rebind to a lakehouse in the target stage without a datasource rule. But Deployment pipelines give me the ich, and also Megan Longoria has already written a post the pains associated with them, so let's focus on the other two options.
Variable libraries can express the reference — an ItemReference stores a workspaceId alongside the itemId, so it can point anywhere — but statically, per value set, maintained by hand, only for three consumer types that don't include semantic models, and only for items that already exist. Which leaves fabric-cicd, whose remote syntax looks like this:
find_replace:
- find_value: "81bbb339-8d0b-46e8-bfa6-289a159c0733"
replace_value:
TEST: "$workspace.Contoso-Data-Test.$items.Lakehouse.Sales.$id"
PROD: "$workspace.Contoso-Data-Prod.$items.Lakehouse.Sales.$id"
Inside one workspace, $items… meant "whoever I'm deployed next to" — environment-agnostic, needing no maintenance. The moment the reference crosses a boundary, $workspace.<display-name> is a literal, case-sensitive workspace name, hard-coded per environment. There is no "the data workspace for this environment". Rename a workspace, or spin up a per-branch feature workspace and you'll be stuck constantly manually managing the parameters.yml file.
Nothing orders a multi-workspace deployment¶
Fabric-cicd deploys one workspace at a time. A solution spanning two is two invocations, and their order is your problem. Microsoft acknowledges this directly: cross-workspace dependencies "require manual orchestration and often cause deployment ordering or binding issues."
Cold start is impossible with up-front resolution¶
Every deploy-time lookup assumes the thing it's looking up already exists. $workspace.<name>.$items… resolves against a remote workspace that must already contain the item; an ItemReference needs a GUID you can only get from a deployed item. Deploy reporting before data has landed and the lookup finds nothing.
For a brand-new environment — disaster recovery, a new tenant, an ISV installing into a customer, a per-branch feature workspace — resolving all references up front is impossible in principle, because the values don't exist until the items are published. The only correct answer is ordered, incremental resolution: publish the lakehouse, then resolve its ID, then publish the model that references it. fabric-cicd resolves at exactly the right moment for this within one call — but nothing orders the calls.
Items in love can't travel together
Within one workspace, two items that depend on each other move between environments and stay bound. Across workspaces, no mechanism carries the relationship through a deployment — not git autobinding (never crosses workspaces), not variable libraries (static GUIDs, three consumer types, manual upkeep), not fabric-cicd (hard-coded remote names, unordered publishes, remote items must pre-exist). The couple can each move house. They just can't move together.
It's worth asking of any Fabric CI/CD design: could this deploy into completely empty workspaces, from the repo alone? If not, you don't have a reproducible solution — you have an environment that was hand-grown once and is now being mutated.
Summary¶
| Reference type | Autobinding | Variable Library | fabric-cicd |
|---|---|---|---|
| Same-workspace item → item, supported pair | Free | Unnecessary | Unnecessary |
| Same-workspace, "Partial" (by name / path / URI) | Only if naming matches | Sometimes | Safer |
| Same-workspace item → item, non-binding pair | Only 3 consumer types | Preferred, dynamic $items… |
|
| Semantic model connection strings | Can't consume variables | Only option | |
| Dataflow Gen2 destinations | Only option | ||
| Connections / gateways | Never | Connection reference, but notebooks + UDFs only | ID yes, credentials never |
| Cross-workspace item → item | Never | Static GUIDs, 3 consumers | Hard-coded workspace names |
| Cross-workspace, environment-varying workspace names | Not covered | ||
| Ordering across workspaces | Not covered | ||
| Cold start into empty workspaces | Items yes, references wrong | One workspace per call, order manual |
The single-workspace binding story has quietly become good. The multi-workspace story is where the platform still shrugs and hands you says "This is your problem."