forcedotcom/sf-skills4 files

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 bundle packageable, choosing managed/unlocked flavor, creating package/version with sf package, installing/upgrading in another org, and debugging failures. TRIGGER on packaging, 2GP, managed package, unlocked package, AppExchange, package version, sf package, sf package install, sf package upgrade, install a package, upgrade a package, cross-org distribution. DO NOT TRIGGER for plain source deploy to one org (use experience-ui-bundle-deploy) or scaffolding a new bundle (use experience-ui-bundle-project-generate). Apply piecemeal — do ONLY the part asked.

Specification
Skill ID
forcedotcom/sf-skills/experience-ui-bundle-2gp-deploy
Publisher
forcedotcom
Repository
sf-skills
Installs
0
Files
4
Synced
Sep 23, 2026
How to use it

Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.

forcedotcom/sf-skills/experience-ui-bundle-2gp-deployInstalls these files
  • SKILL.md
  • assets/CustomApplication.app-meta.xml
  • assets/PermissionSet.permissionset-meta.xml
  • scripts/find-bundle-package-dir.sh

What this skill tells the agent

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. unlockedChoose a flavor
Make the bundle packageable / wire a CustomApplicationPart 1
Create the package or a new versionPart 2
Install / upgrade / uninstall / promotePart 3
Debug a failurePart 4 + 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:

packageDir="$(scripts/find-bundle-package-dir.sh <bundleName>)"   # walks packageDirectories; picks the entry whose tree contains uiBundles/<bundleName>/

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.
sf org list                                            # connected orgs + default Dev Hub
sf org list --json | jq -r '.result.nonScratchOrgs[]?.alias'

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:

# clean "0 records" = 2GP ON;  "sObject type 'Package2' is not supported" = toggle OFF
sf data query --target-org devhub --use-tooling-api --query "SELECT Id FROM Package2 LIMIT 1"
sf org display --target-org devhub --json | jq '.result.isDevHub'

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.

ManagedUnlocked — namespacedUnlocked — org-dependent
Namespacerequiredrequirednone (empty "")
Source visibilityhidden (IP-protected)visible / editablevisible / editable
Installs asns__Namens__Namebare Name (flat)
Coexists with a local same-name bundleyes (ns-filtered)yes (ns-filtered)no — collides
Upgrade behaviorclean replace (locked)replace, overwrites subscriber editsreplace, overwrites subscriber edits
Rollback risk on failed upgradeyesyesnone
Typical useISV / AppExchange distributionorg-agnostic sharing, source openpackage 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).

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

More from forcedotcom

Use this skill in a project

Open any RiverX project, search the Skills panel for forcedotcom/sf-skills/experience-ui-bundle-2gp-deploy, and install it in one click.

Use this skill