Reading time: 9 min read

Sitecore CLI is a versatile replacement for the Package Manager

A fond look back at Package Designer and the Installation Wizard, and a practical guide to doing all of it with <dotnet sitecore> instead.

Portrait photo of David Austin, article author

Say goodbye to Package Manager

If you have been around Sitecore long enough, you have a muscle memory that goes something like this: open the Sitecore Desktop, click the Start menu, drift down to Development Tools, and land on Package Designer. Or the faster version, Installation Wizard, because someone just emailed you a .zip and said "can you pop this on staging real quick."

That workflow was, for a solid fifteen years, the universal translator of the Sitecore world. It did not care whether you were an admin, a developer, or a content author. It spoke one language: put stuff in a zip, move the zip, unzip the stuff somewhere else.

How admins used it

  • Installing modules. Web Forms for Marketers, EXM, SXA, Sitecore PowerShell Extensions, Publishing Service assets, and the long tail of community modules all shipped as packages. Download, upload, install, restart, done.
  • Applying hotfixes and cumulative patches through .update packages.
  • Moving security roles and domain definitions between environments, which was one of the few clean ways to keep role structures in sync.
  • Taking a quick snapshot before doing something risky. Build a package of the branch you are about to touch, stash the zip on a file share, sleep better.

How developers used it

  • Shipping a feature that included both items and files. Templates, renderings, layouts, plus the DLLs and views that backed them, all in one artifact.
  • Handing QA a reproducible slice of content so a bug could actually be reproduced.
  • Delivering to clients who did not give you deployment access. "Here is the package, here are the install notes, here is my phone number."
  • Seeding a new environment. Spin up a fresh instance, install the baseline content package, and you have something that resembles production.
  • Post-steps, which were a genuinely useful escape hatch. A package could run code after install to rebuild an index, publish a subtree, or fix up a setting.

How authors and content ops used it

This is the part people forget. Package Manager was not just a developer tool.

  • Content authors on staging would package up a finished campaign section and hand it to an admin to install on production. Not elegant, but it worked, and it did not require a release.
  • Media Library transfers. Someone uploads 400 images to the wrong environment, and a package is the least painful way to fix it.
  • Sharing a component configuration between two sites in the same tree, or between two clients entirely.
  • Emergency content restore. "We deleted the wrong folder." If someone had packaged it the week before, you were a hero in about four minutes.

It also outlived the era people assume it died in. Long after the conversation had moved to serialization and pipelines, the classic shell was still sitting there behind the Sitecore Cloud Portal, and for a good while you could still reach the package tools through it. There was never a single day where it stopped working and everybody had to go learn something new, which is a large part of why the habit stuck around as long as it did.

Sadly that era is gone and it is not coming back. It maybe caught people off guard, impacting authors and such the most, but for the rest of us admins and developers we were left with two options. Learn how to adapt to the new Content Transfer API or at the very least, really get to know the Sitecore CLI (aka dotnet sitecore).

Now that's not to say there aren't tools out there, on the Sitecore Marketplace or in the wild. Even I built a simple command line JavaScript script to transfer content from a to b, and when I'm promoting content for testing other, it's my go to. I won't be going into that today though. Today, I will be looking at how one can very, VERY easily utilize the Sitecore CLI to move and backup content as needed. Quickly, and painlessly.

Connecting to cloud environments

Getting set up

If you're using SitecoreAI within a Docker container, you likely will have all this setup already after performing a successful init and up. If not, and you're just working with the front-end and connecting to SitecoreAI remotely, then you'll need to install the Sitecore CLI. Installation is super simple and takes less than 5 min to do. Highly recommended doing it in an console running with administrative priviledges though.

dotnet new tool-manifest
dotnet tool install Sitecore.CLI

Then initialize and add the plugins you need:

dotnet sitecore init
dotnet sitecore plugin add -n Sitecore.DevEx.Extensibility.Serialization
dotnet sitecore plugin add -n Sitecore.DevEx.Extensibility.Publishing

For XM Cloud work, add the cloud plugin as well:

dotnet sitecore plugin add -n Sitecore.DevEx.Extensibility.XMCloud

Connecting to XP / XM

If you're working with an XP or XM environment, you'll need to login to it via this method. This is considered interactive login, and it works fine for day to day work, which opens a browser and runs a device flow:

dotnet sitecore login --authority https://id.example.com --cm https://cm.example.com --allow-write true

Connecting to SitecoreAI

If you're working with SitecoreAI, you'll want to login via the following methods.

dotnet sitecore cloud login

A browser window will open, prompting you to login to the organization you are working with. You will want to be connecting to the right organization otherwise your environment connect will not work. You can then connect to the environment you want to via the following command. You will get the environment ID from the SitecoreAI Deploy Portal, selecting the right project and environment and then navigating to the Details tab.

dotnet sitecore cloud environment connect --environment-id $XMC_ENVIRONMENT_ID --allow-write

This will update the ./sitecore/user.json with the environment details allowing you to be able to push and pull from that environment. Here's an example of what one might look like. In most Docker environments, the default environment will be your local Docker container environment.

{
  "endpoints": {
    "xmCloud": {
      "allowWrite": false,
      "host": "https://xmclouddeploy-api.sitecorecloud.io/",
      "authority": "https://auth.sitecorecloud.io/",
      "accessToken": "x.x.xxxxxxxxx-x",
      "refreshToken": "v1.x",
      "refreshTokenParameters": {
        "organization_id": "org_xxxxxxxxxxxxxxxx"
      },
      "expiresIn": 86400,
      "lastUpdated": "2026-09-11T16:49:25.0423168Z",
      "clientId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "variables": {
        "xmCloudMonitoringHost": "https://xmcloud-monitoring-api.sitecorecloud.io/"
      },
      "audience": "https://api.sitecorecloud.io"
    },
    "default": {
      "ref": "xmcloud",
      "allowWrite": true,
      "host": "https://xmcloudcm.localhost",
      "variables": {}
    },
    "qa": {
      "ref": "XmCloud",
      "allowWrite": true,
      "host": "https://xmc-xxxxxxxxxxxxxxx-xxxxxxxxxx-xxxxx.sitecorecloud.io/",
      "variables": {}
    },
    "preprod": {
      "ref": "XmCloud",
      "allowWrite": true,
      "host": "https://xmc-xxxxxxxxxxxxxxx-xxxxxxxxxx-xxxxx.sitecorecloud.io/",
      "variables": {}
    },
    "prod": {
      "ref": "XmCloud",
      "allowWrite": true,
      "host": "https://xmc-xxxxxxxxxxxxxxx-xxxxxxxxxx-xxxxx.sitecorecloud.io/",
      "variables": {}
    }
  },
  "$schema": "./schemas/UserConfiguration.schema.json",
  "defaultEndpoint": "default"
}

Setting up a module

The modules, those files that end in .module.json are what determine the items that are included in the serialization. Here's a minimized example of one. In this example we created a ContentBackup namespace that will pull the Home and Data folders and their descendants.

Super handy not only for doing content backups, but also for loading up our local instance with production content.

{
  "namespace": "ContentBackup",
  "items": {
    "includes": [
      {
        "name": "$ContentBackup/Home",
        "path": "/sitecore/content/Collection/Site/Home",
        "scope": "ItemAndDescendants",
        "allowedPushOperations": "CreateAndUpdate"
      },
      {
        "name": "$ContentBackup/Data",
        "path": "/sitecore/content/Collection/Site/Data",
        "scope": "ItemAndDescendants",
        "allowedPushOperations": "CreateAndUpdate"
      }
    ]
  }
}

Backing up a SitecoreAI environment

Now, let's say we're in the middle of a release, and we want to backup our environment we've connected to above before we do a code push to it. Let's say, for example sake, that that environment is prod.

dotnet sitecore ser pull -i ContentBackup -n prod

The above call, would use the ContentBackup namespace and pull the Home and Data folders from prod.

If you needed to zip it up and store it, you could. If you needed to download the content, zip it and send it to another dev for them to load into their system, using a pull command, you could. You'd just do the following:

dotnet sitecore ser push -i ContentBackup -n <local environment name>

You have a lot of flexibility with the above both as an admin and as a dev. While not ideal as a content author, with good enough training and documentation setup, it's not impossible though.

Using Sitecore CLI with a non-interactive user

Outside of a local environment, we can utilize the dotnet sitecore functionality without requiring a user to actually take part in the login and subseqeunt pull and push services. We do that with a non-interactive user. For automation you use the client credentials flow against a dedicated identity, which is the modern equivalent of the service account that used to install your packages.

On XM and XP

You register a client on Sitecore Identity Server with the client_credentials grant and map it to a Sitecore user, so item-level security still applies.

The config patch drops into the Identity Server config folder and looks roughly like this:

<Settings>
  <Sitecore>
    <IdentityServer>
      <Clients>
        <AutomationClient>
          <ClientId>SitecoreAutomation</ClientId>
          <ClientName>Release Pipeline</ClientName>
          <AccessTokenType>0</AccessTokenType>
          <AccessTokenLifetimeInSeconds>3600</AccessTokenLifetimeInSeconds>
          <RequireClientSecret>true</RequireClientSecret>
          <ClientSecrets>
            <ClientSecret1>REPLACE_FROM_SECRET_STORE</ClientSecret1>
          </ClientSecrets>
          <AllowedGrantTypes>
            <AllowedGrantType1>client_credentials</AllowedGrantType1>
          </AllowedGrantTypes>
          <AllowedScopes>
            <AllowedScope1>sitecore.profile.api</AllowedScope1>
          </AllowedScopes>
          <Claims>
            <Claim1>
              <Type>name</Type>
              <Value>sitecore\automation</Value>
            </Claim1>
          </Claims>
        </AutomationClient>
      </Clients>
    </IdentityServer>
  </Sitecore>
</Settings>

Then the pipeline logs in with no browser involved:

dotnet sitecore login --authority https://id.example.com --cm https://cm.example.com --allow-write true --client-credentials true --client-id SitecoreAutomation --client-secret $SITECORE_CLIENT_SECRET

On SitecoreAI

Same idea exists in SitecoreAI, except you don't have the same config / path file requirements, different front door. You create an automation client in the SitecoreAI Deploy portal, which gives you a client ID and secret, and the CLI takes them straight from environment variables:

dotnet sitecore cloud login --client-credentials --client-id $SITECORE_CLOUD_CLIENT_ID --client-secret $SITECORE_CLOUD_CLIENT_SECRET
dotnet sitecore cloud environment connect --environment-id $XMC_ENVIRONMENT_ID --allow-write

After connect, the environment is registered in your local user.json and every ser command behaves exactly as it does against a self-hosted instance.

What an automation backup script might look like

Once you have the connection in place, you could pair it with an automation backup. So following your login and if needed, connection, you could then perform the following to generate the backup and store the .itempackage which is the equivalent of that .zip file we fondly remember, in a specific place.

$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$backupDir = "./artifacts/backup-$stamp"

# Capture the current state of everything in scope
dotnet sitecore ser pull -i ContentBackup -n prod

# Turn it into a single portable artifact, storing in a unique date specific folder
New-Item -ItemType Directory -Force -Path $backupDir | Out-Null
dotnet sitecore ser package create -o "$backupDir/contentbackup.itempackage"

Write-Host "Backup written to $backupDir/contentbackup.itempackage"

But keep in mind, a push or a package install of said package (i.e. contentbackup.itempackage), brings items back to their captured state. It does not resurrect items that were never captured, and depending on your allowedPushOperations it may not remove items created after the snapshot was taken. It is a content restore, not a database restore.

At the end of the day

The Package Manager earned its reputation because it was solid, it worked and could be counted on. It was one tool, in one place, that solved a problem for every role on the project, and there is a reason people still reach for the Desktop out of habit. Losing it feels like losing something, because it is.

But look at what the CLI hands back. Every content move is a command somebody can read. Every release carries a snapshot. The service account has exactly the permissions it needs and no more. Structure travels with the code that depends on it. And the whole thing runs identically on your laptop, on a build agent, and against XM Cloud.