How to Maintain Custom Libraries Across Fabric Workspaces

If you’ve been building shared code for your team in Fabric, you already know the easy part is writing the library. The hard part shows up later: keeping that same library consistent across a dozen workspaces without turning every release into a manual chore.

I’ve covered how to work as a team in an enterprise way and how to build your own library for Fabric in earlier posts. This one picks up where those left off – how do you actually maintain those libraries once they’re spread across your tenant?

There are two strategies I lean on, and I switch between them depending on the setup. Neither is “the right one” – they solve slightly different problems.

Two Ways to Manage Libraries Across Workspaces

Before we get into the wiring, here’s the short version of both approaches:

  • One shared environment – you create an environment in a central workspace and point notebooks from other workspaces at it. Update it once, everyone gets the change.
  • One shared library, many environments – you keep the .whl in a single place and use a script to push it out to environments living in each workspace. More moving parts, but far more flexible on the compute side.

Let’s take each one in turn.

Architecture 1: A Single Shared Environment

This is the simpler of the two, and honestly it’s where I’d start most teams.

You spin up a workspace that holds shareable items, and one of those items is a shareable environment (or a few of them, if you have different workload profiles). Because everything points back to one place, maintenance is painless – any update to the environment happens in a single spot, and every notebook connected to it immediately picks up the new Spark cluster, library, or resource.

The trade-off is configurability. If you want a cluster defined independently while reusing the same shared library, this isn’t the way to go – that’s exactly the gap the second architecture fills. But if you’re after simplicity and a single org-wide standard, this gets you there fast.

Architecture with a single shared environment
Pasted image 20260525193245.png

You can see it in the diagram above: multiple notebooks all pointing at the same shared workspace, which holds the shared library the developers use. Wiring a notebook to it takes two clicks:

  1. Open the notebook.
  2. Expand the environment list and pick the shared environment. That’s it – your custom libraries from that environment are now available.
Pasted image 20260531114732.png

There’s a natural extension here. Instead of one shared environment, create a few – one per workload type. Small jobs get a lightweight Spark config, heavy loads get something beefier. The setup is identical; the only difference is that your shared workspace now holds more than one environment. Pick whichever one matches the job.

Architecture 2: One Library, Pushed to Every Workspace

The second approach is the one I reach for on larger tenants. You still want reusable code sitting in one place, and you still want every workload running the latest version – but now you also want full control over each environment’s Spark engine and resources. A single shared environment can’t give you both.

Pasted image 20260627135418.png

Here’s the idea. On a shared workspace you store the .whl library file (add versioning here if you like), plus a script that publishes it out to environments in other workspaces. You decide which environments are in scope. In my sample script I push it across the whole tenant – personal workspaces excluded – wherever I find an environment with a DE suffix.

Let me walk through the setup.

First, create the target environments in your workspaces using a consistent naming convention. I use a DE suffix. You can use your own rule, but heads up – the script matches on _DE, so if you change the convention you’ll need to adjust it to match.

Pasted image 20260704140443.png

Next, create a notebook in your “shared tool” workspace. It can technically live anywhere, but I keep all reusable items in one workspace so the team always knows where to look. Once the notebook exists, attach your library to it as a built-in resource:

Open the notebook → go to the Resources tab in the explorer → click the three dots next to Built-in → click Upload files → select your library.

Pasted image 20260704140738.png

With the prerequisites in place, here’s the script (This scripts exists as well in my GH Repo – Github):

Python
# ---- 0. Import libraries ----------------------------------------------
import re
import requests
import notebookutils

# ---- 1. Parameters ----------------------------------------------------
WHL_NAME = "dataguide_demolibrary-0.2.0-py3-none-any.whl"  # new version, sitting in notebook Resources
ENV_PATTERN = "_DE"  # match environments named *_DE (suffix match)

# ---- 2. Setup ---------------------------------------------------------
BASE = "https://api.fabric.microsoft.com/v1"
TOKEN = notebookutils.credentials.getToken("pbi")  # token for the current user, no service principal needed
H = {"Authorization": f"Bearer {TOKEN}"}

whl_path = f"{notebookutils.nbResPath}/builtin/{WHL_NAME}"
whl_bytes = open(whl_path, "rb").read()
package = re.split(r"[-_]\d", WHL_NAME)[0]  # "my_library" -> used to find & remove old versions
print(f"Wheel: {WHL_NAME} ({len(whl_bytes)/1024:.0f} KB) package='{package}'")

# ---- 3. Helpers -------------------------------------------------------
def get_all(url):
    """GET a Fabric list endpoint, following pagination."""
    items = []
    while url:
        r = requests.get(url, headers=H); r.raise_for_status()
        body = r.json()
        items += body.get("value", [])
        url = body.get("continuationUri")
    return items

def deploy(ws_id, env_id):
    libs = f"{BASE}/workspaces/{ws_id}/environments/{env_id}/staging/libraries"
    # 3a. remove any existing version of the same package
    existing = requests.get(f"{libs}?beta=false", headers=H).json().get("libraries", [])
    for lib in existing:
        name = lib.get("name", "")
        if (lib.get("libraryType") == "Custom" and name.endswith(".whl")
                and re.split(r"[-_]\d", name)[0] == package):
            requests.delete(f"{libs}/{name}", headers=H).raise_for_status()
    # 3b. upload the new wheel
    up = requests.post(f"{libs}/{WHL_NAME}",
                       headers={**H, "Content-Type": "application/octet-stream"},
                       data=whl_bytes)
    up.raise_for_status()
    # 3c. publish so attached notebooks pick up the change
    requests.post(f"{BASE}/workspaces/{ws_id}/environments/{env_id}/staging/publish",
                  headers=H).raise_for_status()

# ---- 4. Scan every workspace, deploy to each *_DE environment ---------
updated = []
for ws in get_all(f"{BASE}/workspaces"):
    try:
        envs = get_all(f"{BASE}/workspaces/{ws['id']}/environments")
    except Exception:
        continue  # skip workspaces that don't expose environments
    for env in envs:
        if env["displayName"].endswith(ENV_PATTERN):
            try:
                deploy(ws["id"], env["id"])
                updated.append(ws["displayName"])
            except Exception as e:
                print(f" ! Failed in {ws['displayName']} / {env['displayName']}: {e}")

# ---- 5. Report --------------------------------------------------------
print(f"\nUpdated {WHL_NAME} in {len(updated)} workspace(s):")
for name in sorted(updated):
    print(f" - {name}")
print("\nPublish runs in the background (typically a few minutes per environment).")

Run it, and you get a printed list of the workspaces the library landed in. It finishes fast – all it does is upload and kick off the publish. The publish itself runs in the background, so the script never sits there waiting on it.

Pasted image 20260704141343.png
Pasted image 20260704143024.png

Updating to a New Version

The same script handles version bumps – no second script to maintain. After you update the library, just run it again. It looks for the package by name (the part before the version, which is why a consistent naming convention matters), finds it in the environments that are in scope, removes the old one, and uploads the new build. Publish happens in the background here too.

Upload the new version and update the variable
Pasted image 20260704143327.png

Run the script
Pasted image 20260704143538.png

Check the results
Pasted image 20260704143519.png

Wrapping Up

That’s the two approaches I use for keeping custom libraries consistent across a Fabric tenant. One shared environment when you want simplicity and a single standard. One shared library pushed to many environments when you need the reusable code centralized but the compute flexible per workspace.

Which one fits your day-to-day? Or are you handling this some other way entirely? Drop it in the comments – always curious how other teams tackle this.

Happy automating!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *