experience-ui-bundle-2gp-deploy
MUST activate when the user wants to package, distribute, or install/upgrade/uninstall/promote a UI Bundle as a Salesforce second-generation (2GP) package (project may contain uiBundles/ or sfdx-project.json for packaging tasks; install/upgrade tasks may lack local bundle files). Handles making a bu
By forcedotcom · 1,670 installs
npx skills add forcedotcom/sf-skills --skill experience-ui-bundle-2gp-deploy
Source repository · Upstream listing
Package an existing UI Bundle (2GP)
How to take a UI Bundle that already exists in the current project
(under <packageDir /uiBundles/<name / , where <packageDir is the package
directory from sfdx project.json — commonly force app/main/default ) and ship
it as a second generation package (2GP), then install / upgrade / uninstall
it in another org.
This is reference knowledge, not a runbook to execute top to bottom. The
user already has a project and a built (or buildable) bundle. Read their intent
and apply only the matching part:
Answer only what was asked. Give the commands for the one part the user
needs plus the org each targets and any genuine caveat — nothing else. Do not
restate the other parts, re explain the flavor table, or replay the full
build→create→install→promote sequence when the user asked about a single step. A
debug question wants the fix, not a packaging tutorial; an install question wants
the sf package install line and the subscriber vs Dev Hub distinction, not
Part 1 and Part 2. Brevity is correctness here.
The user wants to… Go to
Decide managed vs. unlocked [Choose a flavor]( choose a flavor)
Make the bundle packageable / wire a CustomApplication [Part 1]( part 1 make the existing bundle packageable)
Create the package or a new version [Part 2]( part 2 create the package dev hub only)
Install / upgrade / uninstall / promote [Part 3]( part 3 install upgrade uninstall promote)
Debug a failure [Part 4]( part 4 debug inspect) + [Troubleshooting]( troubleshooting)
This skill is for packaging and cross org distribution ( sf package … ). For
plain source deploy of a bundle into one org ( sf project deploy … ), use
experience ui bundle deploy instead. Never sf project generate or
sf template generate ui bundle here — the project and bundle exist.
MyReactApp / force app / force app/main/default are placeholders; substitute
the user's real bundle name and their <packageDir everywhere they appear
below. Resolve <packageDir deterministically — never guess [0] in a
multi package project — with:
Step 0 — Confirm the orgs (do this before touching any org)
Do not assume the default org. Ask the user, or read sf org list , then
restate what you'll use:
Dev Hub ( devhub ) — where the package is created, versions are built, and
source is deployed. Always required.
Subscriber ( subscriber ) — the org you install into. Only required for
install / upgrade / uninstall.
Rules:
Create only task (package or version) → one Dev Hub is enough; don't ask
for a subscriber.
Install task → confirm both , and confirm which is which . Installing
into the Dev Hub by mistake is a common, messy error.
Substitute the real aliases for devhub / subscriber everywhere below.
ID legend (packaging): 0Ho… package · 04t… installable version
(SubscriberPackageVersionId) · 05i… Package2Version · 08c… version create
request · 0Hf… install request · 06y… uninstall request.
ID legend (runtime, useful when debugging a broken subscriber): 9YE… UI
Bundle row · 9YF… UIBundleApplication junction · 02u… CustomApplication /
TabSet · 0Zu… ManagedContentSpace (workspace) · 0ap… ManagedContentChannel
(WEB APP). A missing App Launcher tile after install almost always traces back
to one of these being absent or misprovisioned.
Prerequisite — the 2GP toggle everyone forgets
2GP needs a manual Setup toggle on the Dev Hub that no CLI command or
metadata deploy can flip. Setup → Dev Hub , both on:
1. Enable Dev Hub , and
2. Enable Unlocked Packages and Second Generation Managed Packages ← the real gate.
Until 2 is on, sf package create returns NOT FOUND and any Package2 query
returns sObject type 'Package2' is not supported . There is no CLI workaround —
flip the toggle. Verify before starting:
Choose a flavor
All three are 2GP (same sf package CLI). Pick before creating — it drives the
namespace, how the bundle is named on install, and coexistence.
Managed Unlocked — namespaced Unlocked — org dependent
Namespace required required none (empty "" )
Source visibility hidden (IP protected) visible / editable visible / editable
Installs as ns Name ns Name bare Name (flat)
Coexists with a local same name bundle yes (ns filtered) yes (ns filtered) no — collides
Upgrade behavior clean replace (locked) replace, overwrites subscriber edits replace, overwrites subscriber edits
Rollback risk on failed upgrade yes yes none
Typical use ISV / AppExchange distribution org agnostic sharing, source open package depends on metadata already in the target org
Namespaced flavors (managed, unlocked namespaced) need a namespace registered
and linked to this Dev Hub (App Launcher → Namespace Registries ). No
registered namespace? Use org dependent unlocked — it needs none.
How linking works (namespace ⇄ Dev Hub)
The namespace lives in a separate Developer Edition (DE) org that owns it;
the Dev Hub borrows it via a linked registration. Concretely:
1. Sign up a DE org and register a namespace on it (Setup → Package Manager →
Namespace Registrations).
2. In the Dev Hub, App Launcher → Namespace Registries → Link Namespace ,
log in with the DE org's credentials to link the namespace to this Dev Hub.
3. Set namespace in sfdx project.json to the linked namespace slug. If the
value here isn't linked to the target Dev Hub, sf package version create
fails with a namespace error (see [Troubleshooting]( troubleshooting)).
One DE org can carry multiple namespaces, and one Dev Hub can link multiple DE
orgs — so a single Dev Hub can build packages under several namespaces. The
namespace is locked in at version create time and travels with every UI
Bundle row inside the built version; you cannot change it later.
Runtime model — why the flavor matters
You do not have to explain this to answer a routine question. Reach for it when
the user asks why : why managed hides source, why namespaced installs are
ns Name , why some URLs look different, or why an unlocked upgrade wiped
their edits.
Origin isolation. Every installed UI Bundle renders from its own origin on
.salesforce.app , distinct from salesforce.com core UI. Tiers:
salesforce.com — 1st party core UI
.salesforce.app — 2nd party AFS hosted bundles (no namespace)
<ns .salesforce.app — 3rd party / namespaced (managed + unlocked namespaced)
Because each namespace gets its own subdomain, two bundles from different
packages can coexist without cross origin bleed.
IP protection is a managed only property. For managed packages,
getSourceZip() returns null in subscriber orgs — the compiled dist/ is
stored as opaque content and never handed back. For unlocked (namespaced
or org dependent), the served binary is fully readable by the subscriber.
Install semantics. Managed and unlocked namespaced install as ns Name
and can coexist with a local same name bundle. Org dependent unlocked has no
namespace — it installs as bare Name and collides with a local bundle of
the same developer name.
Delta upgrade. On sf package install of a newer 04t… , the platform
compares content index hashes of each incoming dist/ asset against what's
already stored and skips any asset whose hash is unchanged — a patch that
touches one bundle re writes only that bundle's changed files. Developer owned
artifacts ( dist/ , ui bundle.json , ISV base permission sets) are replaced;
subscriber owned state (subscriber created permission sets, custom metadata,
provisioned domain) is preserved.
Kill switch. Setup → Security → Multi Framework Domains → disable a
provisioned domain. Immediate 404; metadata stays installed; reversible.
Part 1 — make the existing bundle packageable
Prepare the project so it can be packaged and used in org. Apply only what the
request needs.
1a. Set API version + namespace in sfdx project.json
The namespace here decides which flavor you can build (see table above), so set
it deliberately — there is no safe default. Substitute the user's real registered
namespace for <ns ; use "" for org dependent.
Managed / namespaced unlocked → namespace = a registered, linked namespace.
Org dependent unlocked → leave namespace as "" .
Setting a namespace that isn't registered to this Dev Hub fails the build later
(see [Troubleshooting]( troubleshooting)).
1b. Build the bundle — dist/ must exist before packaging
Package or deploy before dist/ exists and the app installs but renders
blank — the bundle ships with its built assets. Always build first.
1c. Wire a CustomApplication (only if the bundle must be launchable as a Salesforce app)
Skip this step when the bundle is already referenced another way (embedded in a
FlexiPage, Experience Cloud site, etc.). Otherwise read
<SKILL DIR /assets/CustomApplication.app meta.xml (where <SKILL DIR is the
absolute path to this skill's own directory), replace every MyReactApp with
the real bundle developer name, and write the result to the user's project
under <packageDir /applications/ . Author <uiBundle with the bundle's
developer name — inside the same package no prefix is needed; cross namespace
it resolves as ns Name (namespaced) or c Name (no namespace).
The three fields the App Launcher tile actually cares about — installed
subscribers won't see a broken tile if they're set correctly:
<uiType Lightning</uiType — required for the App Launcher to render it
<navType Standard</navType — standard navigation container
<formFactors Large</formFactors — desktop form factor (validation is
install time only, so a missing/wrong value passes deploy but hides the tile)
1d. Deploy source to the Dev Hub (so metadata exists before package create )
1e. Grant app visibility via a permission set (only if 1c added a CustomApplication and the app must be reachable without a manual Setup click)
Read <SKILL DIR /assets/PermissionSet.permissionset meta.xml , replace
MyReactApp with the real bundle name (both in <application and the label),
write the result into the user's project, then deploy and assign:
Part 2 — create the package (Dev Hub only)
No subscriber org involved. sf package create runs once (registers the
0Ho… container); you build installable 04t… versions repeatedly after. Pick
the one flavor you chose above:
Then build a version:
version number 1.0.0.NEXT auto bumps the build number; a fixed 1.0.1 pins
it. installation key bypass builds an unprotected version (no key to
install); omit it and pass installation key <key to gate installs.
Robust version create (survives a slow Dev Hub queue)
wait can time out while the build sits queued, losing the request handle.
Submit async, capture the 08c… id, poll:
skip validation is faster but produces a beta version (can't be promoted,
and beta can't upgrade beta — see Part 3). Drop it for a releasable build. Resume
a queued build anytime:
sf package version create report i 08c… target dev hub devhub
Part 3 — install / upgrade / uninstall / promote
Confirm the subscriber alias first (Step 0). Everything here hits the
subscriber — except promote , which runs on the Dev Hub.
Beta can't upgrade beta. A skip validation (beta) v0.2 over a beta v0.1
fails with "Cannot upgrade beta package." Either promote v0.1 (managed) or
uninstall v0.1 first, then install v0.2.
Unlocked upgrades overwrite subscriber edits to the bundle. Org dependent has
no rollback on a failed upgrade; namespaced flavors do.
Robust install (confirm it actually landed)
sf package install wait can exit 0 while the request is still IN PROGRESS —
a false success. Verify