# developer Salesforce Multi-Framework (multi-framework) - Full Documentation # Access Data and Platform Capabilities While Agentforce Vibes automatically generates code for data access, it's helpful to understand how you can write your own code to access Salesforce data and related platform capabilities from your web app. Use the Data SDK for data access with these benefits: - **Abstraction and consistency:** Data SDK abstracts environment-specific APIs, so you call a simple method and the SDK handles the implementation details for the specific host environment, for example, Lightning Experience or an Experience Builder site. - **Capability detection:** Data SDK includes integrated capability detection to identify the runtime environment, infer its capabilities, and determine the concrete way to implement those capabilities. Given the built-in detection, component authors can code safely across deployment scenarios. - **Modern developer experience:** Data SDK provides full TypeScript definitions to offer IntelliSense and autocomplete functionality, compile-time type safety, and uses Promises for structured error handling (try/catch blocks) instead of loosely-typed events. Learn how you can access and update Salesforce data using the Data SDK. - [Work with the Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-intro.md) - [Access Salesforce APIs](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-connect.md) - [GraphQL Query Parameters](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-query.md) - [GraphQL Mutate Parameters](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-mutate.md) - [GraphQL Queries](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-graphql.md) - [GraphQL Mutations](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-graphql-mutation.md) - [Error Handling](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-graphql-error.md) - [Cache Control](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-cache-control.md) - [Per-Request Headers](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-request-headers.md) - [Data SDK Extensions](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-extension.md) Explore platform capabilities for your web app. - [Get App Identity](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-identity.md) - [Manage the Host UI](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-view.md) - [Localize Your App](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-i18n.md) Find out more about the agent skills you can use to speed up data access implementation in your web app. - [Use Skills for Data Access](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-skills.md) # Access Salesforce APIs With Data SDK, you can access any [Connect REST API](https://developer.salesforce.com/docs/platform/connect-rest-api/references/connect-rest-api-about/about.html) resource from your multi-framework app. Many Salesforce features provide their own Connect API resources, such as [Data 360 Connect REST API](https://developer.salesforce.com/docs/data/connectapi/references/spec), [CRM Analytics REST API](https://developer.salesforce.com/docs/atlas.en-us.bi_dev_guide_rest.meta/bi_dev_guide_rest/bi_rest_overview.htm), and [Salesforce Industries REST API](https://developer.salesforce.com/docs/atlas.en-us.api_rest_industries.meta/api_rest_industries/intro.htm). We recommend that you check the reference documentation for the feature you're accessing. Here are common endpoints you can use with Data SDK and the apps they support. See the differences between [internal and external apps](https://developer.salesforce.com/docs/platform/multiframework/mfw-generate-app.md). In the endpoint, `v{version}` represents the API version you work with, such as `v66.0`. | Endpoint | Purpose | Internal (B2E) Apps | External (B2B and B2C) Apps | | :------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------ | :-------------------------- | | `/services/apexrest/*` | [Custom Apex REST resources](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_rest_intro.htm) for accessing your Apex classes and methods | ✓ | ✓ | | `/services/data/v{version}/graphql` | [GraphQL API endpoint](https://developer.salesforce.com/docs/platform/graphql/guide/graphql-about.html) for querying and mutating record data with a single request | ✓ | ✓ | | `/services/data/v{version}/ui-api/records` | [UI API records resources](https://developer.salesforce.com/docs/atlas.en-us.uiapi.meta/uiapi/ui_api_get_started.htm) for reading and writing record data | ✓ | ✓ | | `/services/data/v{version}/ui-api/search-info` | [UI API search resources](https://developer.salesforce.com/docs/atlas.en-us.uiapi.meta/uiapi/ui_api_get_started.htm) for retrieving search suggestions and scope information | ✓ | ✓ | | `/services/data/v{version}/ui-api/layout` | [UI API layout resources](https://developer.salesforce.com/docs/atlas.en-us.uiapi.meta/uiapi/ui_api_get_started.htm) for retrieving metadata-driven page and record layouts | ✓ | ✓ | | `/services/data/v{version}/ui-api/session/csrf` | [UI API CSRF token resource](https://developer.salesforce.com/docs/atlas.en-us.uiapi.meta/uiapi/ui_api_get_started.htm) for protecting requests against cross-site request forgery | ✓ | ✓ | | `/services/data/v{version}/ui-api/*` | [UI API resources](https://developer.salesforce.com/docs/atlas.en-us.uiapi.meta/uiapi/ui_api_get_started.htm) for reading and writing record data, metadata-driven layouts, and related UI data | ✓ | ✓ | | `/services/data/v{version}/chatter/*` | [Chatter Connect API resources](https://developer.salesforce.com/docs/platform/connect-rest-api/references/connect-rest-api-chatter) for accessing Chatter feeds, groups, and users | ✓ | ✓ | | `/services/data/v{version}/chatter/users/me` | [Current Chatter user resource](https://developer.salesforce.com/docs/platform/connect-rest-api/references/connect-rest-api-chatter) for retrieving the profile of the running user | ✓ | ✓ | | `/services/data/v{version}/chatter/users/{userId}` | [Chatter user by ID resource](https://developer.salesforce.com/docs/platform/connect-rest-api/references/connect-rest-api-chatter) for retrieving the profile of a specific user | ✓ | ✓ | | `/sfsites/c/_nc_external/system/security/session/SessionTimeServlet` | [User session timeouts resource](https://help.salesforce.com/s/articleView?id=xcloud.security_overview_sessions.htm) for checking the remaining time before the session expires | ✓ | ✓ | | `/secur/logout.jsp` | Logout for ending the current user session | ✓ | ✓ | | `/services/data/v{version}/connect/file/upload/config` | [File upload config resource](https://developer.salesforce.com/docs/platform/connect-rest-api/references/connect-rest-api-files#tag/Files-Resources) for retrieving the settings needed to upload files | ✓ | ✓ | | `/services/data/v{version}/connect/proxy/ui-telemetry` | UI telemetry proxy for sending client-side performance and usage metrics | ✓ | ✓ | | `/services/data/v{version}/connect/*` | [Connect REST API resources](https://developer.salesforce.com/docs/platform/connect-rest-api/references/connect-rest-api-core) for accessing Salesforce feature data such as files, feeds, and communities | ✓ | | | `/services/data/v{version}/connect/cms/delivery/channels/{channelId}/contents` | [CMS channel content delivery resource](https://developer.salesforce.com/docs/platform/connect-rest-api/references/connect-rest-api-cms) for retrieving published managed content from a channel | | ✓ | | `/services/data/v{version}/connect/cms/channels/{channelId}/searchable-content-types` | [CMS searchable content types resource](https://developer.salesforce.com/docs/platform/connect-rest-api/references/connect-rest-api-cms#tag/CMS-Managed-Content-Resources/operation/getManagedContentTypeSearchableCollection) for retrieving the content types available for search in a channel | | ✓ | We recommend that you use optional chaining `fetch?.()` instead of a non-null assertion `fetch!.()`. Optional chaining is useful when you want to use fetch in a shared or cross-surface utility. If you're not sure where your code is run, use optional chaining and handle the response appropriately. **tip:** The read-data examples in the [Multi-framework recipes repo](https://github.com/trailheadapps/multiframework-recipes) demonstrate how to call Chatter Connect API, Apex REST, and UI API REST. ## See Also - [Work with Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-intro.md) - [Connect REST API Families](https://developer.salesforce.com/docs/platform/connect-rest-api/references/connect-rest-api-about/about.html) # Build Embedded Apps in the Framework You Prefer UI Embedding works with externally hosted apps regardless of the front-end framework they're built in. You can reuse an app you already have instead of rebuilding it in a Salesforce-specific framework. ## Framework Compatibility Because Salesforce embeds your app as an externally hosted app in a sandboxed iframe, it interacts with your app through the standards-based embedding protocol rather than through framework-specific code. Any standards-based web app works. Externally hosted embedding has been validated with React, Angular, and Vue apps. Automated test coverage is React-based. **note:** Framework compatibility applies to externally hosted apps. Internally hosted UI Bundles have their own framework support details. ## How to Use It Point the `src` attribute of `lightning-ui-embedding` at your hosted app, regardless of the framework your app uses. To build and embed the wrapper component, see [Get Started with UI Embedding](https://developer.salesforce.com/docs/platform/microfrontend/guide/get-started.md). ## See Also - [Get Started with UI Embedding](https://developer.salesforce.com/docs/platform/microfrontend/guide/get-started.md) - [Embed an Internally Hosted App as a UI Bundle](https://developer.salesforce.com/docs/platform/microfrontend/guide/internal-ui-bundles.md) # Build the Embedding Bridge Without a Salesforce Package Embed an external web app in Salesforce and talk to the host without adding a `@salesforce/*` package to your app. An embedded app communicates with Salesforce over the `sf-embedding` protocol through a small piece of JavaScript called the bridge: it completes a one-time handshake, then exchanges JSON-RPC 2.0 messages with the host over a `MessagePort`. This guide shows how to build that bridge yourself, in plain JavaScript, and covers the message surface the deployed host implements today. The `sf-embedding` protocol specification normatively defines the behavior described here. Where this guide and the specification disagree, the specification is authoritative. **note:** **Scope.** This guide covers the surface the deployed `lightning-ui-embedding` host implements today: the handshake and version selection, the init lifecycle, host-driven UI state, custom events, resize, and error reporting. The specification defines a larger future surface, including a capability model, host-rendered UI (alert, toast, modal, prompt, and confirm dialogs), host navigation, and request cancellation, that the shipping host doesn't broker yet. Those aren't covered here. The shipping host doesn't gate methods by capability. **note:** This guide covers the guest side only: the bridge that runs inside your embedded app. It doesn't replace the host setup. On the Salesforce side, an admin or developer still renders the `lightning-ui-embedding` base component pointed at your app's URL and adds a CSP Trusted Site for your app's origin. For that path, see [Get Started with UI Embedding](https://developer.salesforce.com/docs/platform/microfrontend/guide/get-started.md). ## When to Build a Bridge by Hand Most apps use the Salesforce-provided guest SDK to talk to the host. Build the bridge yourself when you want full control over the protocol surface, want to avoid an added dependency, or want to integrate the protocol into an existing message layer. A hand-written, wire-level bridge is fully supported as long as it honors the message shapes described here. ## Prerequisites - Your web app is hosted at an HTTPS URL that you control, on an origin that differs from your Salesforce org's origin. - A Salesforce host renders the `lightning-ui-embedding` component pointed at your app, and the org trusts your app's origin for inline frames. See [Get Started with UI Embedding](https://developer.salesforce.com/docs/platform/microfrontend/guide/get-started.md). - You're comfortable working with `window.postMessage`, `MessageChannel`, and JSON-RPC 2.0 message shapes. ## How the Protocol Works Communication happens in two phases. ```text Salesforce host (LWC) Your app (React or any framework) | | | 1. window.postMessage handshake | (bootstrap only) |<---------- heartbeat ------------| |--------- MessagePort transfer -->| | | | 2. JSON-RPC 2.0 over the port | (everything else) |<================================>| ``` 1. **Handshake (`window.postMessage`).** The host loads your app in a cross-origin iframe and passes `hostMetaData` (which includes `hostAppOrigin` and `instanceId`) on the URL. Your app sends an `sf-embedding/ready` heartbeat, and the host replies by transferring a `MessagePort`. 2. **Session (JSON-RPC 2.0 over the port).** Every later message, whether a request, a response, or a notification, is standard JSON-RPC 2.0 framed over that port. `MessageChannel` delivers messages in order per port. ### Do You Need JSON-RPC? A JSON-RPC library isn't required, but the JSON-RPC 2.0 message shape is required if you want to talk to the real Salesforce host. Keep two things separate. - **The message shape is the host's contract.** The deployed `lightning-ui-embedding` host emits and parses JSON-RPC 2.0 on the port. Requests are `{ jsonrpc: "2.0", id, method, params }`, notifications drop the `id`, and responses carry the `id` plus a `result` or an `error`. The `id` is a monotonic, non-negative integer per side. A request can carry an optional `_meta.traceId`, which the host echoes on the reply. The host doesn't understand anything else on the port. That's a data format, not a dependency, so you can hand-build those objects, which is what the bridge in Step 2 does with no library. - **The handshake is not JSON-RPC.** The heartbeat and bootstrap envelope are plain `window.postMessage` messages. JSON-RPC begins only after the `MessagePort` transfer. ## Step 1\: Complete the Handshake The handshake is Salesforce-specific and security-critical, so follow it exactly. Read `hostMetaData`, send the heartbeat, and then wait for the `MessagePort` inside an `sf-embedding.bootstrap` envelope. Validate the envelope with five checks before you trust it. ```javascript // Runs at document start, before any bridge is created. function bootstrap({ signal } = {}) { return new Promise((resolve, reject) => { if (window.parent === window) { // Loaded directly, not embedded. Stay silent so your app still works standalone. return reject(new Error("NOT_EMBEDDED")); } // hostAppOrigin pins where the heartbeat is sent and is the only trusted origin for this session. // instanceId binds the incoming envelope to this specific embed. const meta = JSON.parse(new URL(location.href).searchParams.get("hostMetaData") ?? "null"); const hostAppOrigin = meta?.hostAppOrigin; const instanceId = meta?.instanceId; if ( typeof hostAppOrigin !== "string" || !hostAppOrigin || typeof instanceId !== "string" || !instanceId ) { return reject(new Error("BAD_HOST_METADATA")); // fail fast; don't half-initialize } let captured = false; function onMessage(event) { const d = event.data; // Five checks; all must pass before you trust the port: if (d?.type !== "sf-embedding.bootstrap" || d?.protocol !== "sf-embedding") return; // 1. type and protocol if (event.origin !== hostAppOrigin) return; // 2. only the host origin from hostMetaData if (event.source !== window.parent) return; // 3. only from the parent window if (event.ports.length !== 1) return; // 4. exactly one transferred port if (d.instanceId !== instanceId) return; // 5. bound to this embed if (captured) { // A second valid envelope means someone is racing the host. You can't tell which is real, // so fail closed: tell the host to shut this session down, and don't use the new port. parent.postMessage( { type: "sf-embedding/shutdown", reason: "DUPLICATE_PORT_TRANSFER" }, hostAppOrigin, ); return; } captured = true; const port = event.ports[0]; port.start(); resolve({ port, hostAppOrigin, instanceId }); // Keep the listener registered so a later duplicate envelope still triggers the shutdown above. } window.addEventListener("message", onMessage); signal?.addEventListener("abort", () => reject(new Error("BOOTSTRAP_ABORTED")), { once: true }); // Send the heartbeat only after the listener is registered, so you can't miss the reply. // protocolVersion here is how you select the protocol version; there is no separate handshake for it. parent.postMessage( { type: "sf-embedding/ready", instanceId, protocolVersion: "1.0.0" }, hostAppOrigin, ); }); } ``` **important:** There's no bootstrap timeout. Silence isn't failure. The iframe can legitimately sit on a login, multi-factor authentication (MFA), or consent screen before your app loads and sends its heartbeat. Run the handshake once, as early as possible, such as a module-load side effect or a mount effect on your top-level layout, and reuse the single session promise everywhere. If you bootstrap lazily on first feature use, any screen that doesn't touch the bridge on mount never handshakes. The failure is invisible because no error fires and the feature does nothing. ## Step 2\: Build the Bridge After the handshake, communication is plain JSON-RPC 2.0, so the bridge is small. It generates request IDs, correlates replies, dispatches inbound requests and notifications by method, and times out requests. Here's the whole bridge, hand-written with no dependencies. ```javascript function createBridge(port) { let nextId = 1; const pending = new Map(); // id -> { resolve, reject } const handlers = new Map(); // method -> (params) => result | Promise // Don't JSON.stringify. MessagePort uses structured clone; pre-stringifying breaks it. port.onmessage = ({ data: msg }) => { if (msg.id != null && ("result" in msg || "error" in msg)) { // A reply to a request we sent. const p = pending.get(msg.id); if (!p) return; pending.delete(msg.id); msg.error ? p.reject(msg.error) : p.resolve(msg.result); } else if (msg.method) { // An inbound request (has id) or notification (no id) from the host. Promise.resolve(handlers.get(msg.method)?.(msg.params)).then((result) => { if (msg.id != null) port.postMessage({ jsonrpc: "2.0", id: msg.id, result }); }); } }; return { // Fire-and-forget. notify: (method, params) => port.postMessage({ jsonrpc: "2.0", method, params }), // Request and response. All shipping requests are quick, so one default timeout is enough. request: (method, params, timeoutMs = 30_000) => new Promise((resolve, reject) => { const id = nextId++; pending.set(id, { resolve, reject }); if (timeoutMs) setTimeout(() => { if (pending.delete(id)) reject(new Error(`TIMEOUT ${method}`)); }, timeoutMs); port.postMessage({ jsonrpc: "2.0", id, method, params }); }), // Handle inbound host requests and notifications. on: (method, handler) => handlers.set(method, handler), }; } ``` **important:** Don't `JSON.stringify` payloads. `MessagePort.postMessage` runs the structured clone algorithm, which preserves `Date`, `Map`, `Set`, and typed arrays. Pass JSON-RPC objects to it directly, and read `event.data` directly rather than re-parsing a string. Every value you send must be structured-clone-safe. **note:** Prefer a maintained library? Any conformant JSON-RPC 2.0 library works, and the rest of this guide is identical. Whatever you use, pin the version, and confirm that it emits standard JSON-RPC 2.0, never auto-retries (which can duplicate a side effect), and never pre-stringifies payloads. ## Step 3\: Complete the Init Lifecycle and Subscribe to Host State Right after the port is live, the host announces itself and you announce back. Then you subscribe to the host's rendering state. Register your inbound handlers before you announce, so a fast host can't get ahead of you. - The host sends `ui/notifications/host-initialized`, the authoritative carrier of session identity (`instanceId` and `hostInfo`). - You reply once with `ui/notifications/embedding-initialized` (your app's name and version). - You subscribe to `ui/subscribe/ui-state` to receive the host's props and styles. The current snapshot arrives in the response, not as a separate notification. ```javascript const { port } = await bootstrap(); const bridge = createBridge(port); // Register inbound handlers before announcing, so a fast host can't get ahead of you. bridge.on("ui/notifications/host-initialized", (p) => { /* p.instanceId, p.hostInfo */ }); bridge.on("ui/notifications/ui-state-changed", ({ current }) => render(current.props, current.styles), ); bridge.on("ui/events/dispatch", ({ eventType, detail }) => handleHostEvent(eventType, detail)); // Announce your app once. bridge.notify("ui/notifications/embedding-initialized", { embeddingInfo: { name: "my-app", version: "1.0.0" }, }); // Subscribe to host-driven props and styles; the snapshot arrives in the response. const { subscriptionId, current } = await bridge.request("ui/subscribe/ui-state", {}); render(current.props, current.styles); ``` **note:** There's at most one active UI-state subscription per session. A second `ui/subscribe/ui-state` while one is active rejects with `INVALID_PARAMS`. Snapshots are full replacements, never diffs, and the host batches a multi-property update in one JavaScript turn into a single `ui/notifications/ui-state-changed`. If you send a request before the init lifecycle finishes, the host rejects it with `NOT_INITIALIZED`. Wait for `ui/notifications/host-initialized`, and then send your requests. ## Use the Bridge With the bridge in place, every protocol method is a request or a notification. ```javascript // Request with a default 30-second timeout. Subscribe to host-driven state: const { current } = await bridge.request("ui/subscribe/ui-state", {}); // Notification, fire-and-forget. Hint a new size: bridge.notify("ui/notifications/resize", { height: 800 }); // Notification. Bubble a custom event onto the host element: bridge.notify("ui/events/dispatch", { eventType: "orderselected", detail: { id: "O-123" } }); // Receive an event the host forwards to you (after ui/events/subscribe): bridge.on("ui/events/dispatch", ({ eventType, detail }) => handleHostEvent(eventType, detail)); ``` A few rules are worth internalizing. - **Every request gets a response. Notifications never reply.** A request resolves with a `result` or rejects with an `error`. Only notifications are dropped silently, because they have no response path. - **One default timeout is enough.** All shipping requests are quick, and the shipping host has no user-gated (indefinite) requests, so you don't need to extend or disable the timeout for any method. - **Notifications are fire-and-forget.** `ui/notifications/resize` is a size hint; the host applies its own layout policy and can honor, clamp, or ignore it. There's no confirming response, so observe the outcome through `window.innerWidth` and `window.innerHeight` if you need to. - **Dirty-state rides on the event channel.** There's no dedicated method for it. Dispatch `ui/events/dispatch` with `eventType: "trackdirtystate"` and `detail: { isDirty, instanceId, label }`. Use the `instanceId` from the handshake. Omit it and the host drops the signal. The host re-emits `trackdirtystate` as a DOM event that a page author can bind to. ## Protocol Messages These are the messages a bridge uses over the port. None of them needs a Salesforce package, because you send and receive them all through the bridge. The handshake messages (the `sf-embedding/ready` heartbeat and the `sf-embedding.bootstrap` envelope) travel over `window.postMessage`, not the port. Every message in the table is JSON-RPC 2.0 over the port. | Message | Direction | Kind | Purpose | | ---------------------------------------- | ----------- | ------------ | ---------------------------------------------------------------------------- | | `ui/notifications/host-initialized` | host to app | notification | Host announces itself (`instanceId`, `hostInfo`) | | `ui/notifications/embedding-initialized` | app to host | notification | Your app announces itself (name, version) | | `ui/subscribe/ui-state` | app to host | request | Subscribe to host-driven props and styles; the response carries the snapshot | | `ui/unsubscribe/ui-state` | app to host | request | Dispose the UI-state subscription | | `ui/notifications/ui-state-changed` | host to app | notification | Full UI-state snapshot on every change | | `ui/events/subscribe` | app to host | request | Subscribe to receive host-forwarded custom events | | `ui/events/unsubscribe` | app to host | request | Dispose the events subscription | | `ui/events/dispatch` | both | notification | Custom event either way; dirty-state rides on this | | `ui/notifications/resize` | app to host | notification | Fire-and-forget size hint (width and height in CSS pixels) | | `ui/notifications/error` | app to host | notification | Sanitized error report (no message or stack; capped at about 50 per session) | **note:** This table is the full surface the deployed `lightning-ui-embedding` host implements today. The protocol specification defines a larger future surface, including a capability model, host-rendered UI (alert, toast, modal, prompt, and confirm dialogs), host navigation, and request cancellation, that the shipping host doesn't broker yet. ## Error Responses Every request gets a response. A failure comes back as a JSON-RPC error with a numeric `code`, an optional `message`, and optional `data` (`{ retryable?, details? }`). These are transport-level codes on the port, distinct from the component error codes reported on the `sf-embedding.component.error` DOM event. For those, see [UI Embedding Error Codes](https://developer.salesforce.com/docs/platform/microfrontend/references/microfrontend-error-codes/error-codes.html). | Code | Constant | Meaning and what to do | | ------ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | -32700 | `PARSE_ERROR` | Malformed frame received. Fix the payload. | | -32600 | `INVALID_REQUEST` | Not a valid JSON-RPC request object. Fix the envelope. | | -32601 | `METHOD_NOT_FOUND` | Method unknown at the selected version. The feature isn't in this host version. | | -32602 | `INVALID_PARAMS` | Params failed validation, such as a second `ui/subscribe/ui-state` or an unknown `subscriptionId`. Fix your params or subscription logic. | | -32603 | `INTERNAL_ERROR` | Host-side failure handling a valid request. Retry if transient. | | -32012 | `NOT_INITIALIZED` | Request sent before the session finished initializing. Wait for the init lifecycle, then retry. | | -32016 | `PORT_CLOSED` | Session torn down; pending requests reject locally. Remount to reconnect. | | -32017 | `SERIALIZATION_FAILURE` | A payload wasn't structured-clone-safe. Send clone-safe values. | | -32020 | `PROTOCOL_VIOLATION` | Contract broken, such as a duplicate lifecycle message. Fix ordering. | | -32013 | `PROTOCOL_VERSION_MISMATCH` | Heartbeat version not supported by the host. Pin a supported version. Surfaces at the handshake as a component error DOM event, before any port exists. | | -32018 | `REQUEST_TIMEOUT` | Client-synthesized when a request outlives its timeout. Retry with backoff. | `METHOD_NOT_FOUND` and `INVALID_PARAMS` are distinct. `METHOD_NOT_FOUND` means the host doesn't know the method. `INVALID_PARAMS` means the method exists but your arguments are wrong. `REQUEST_TIMEOUT` is a client-side convention your bridge synthesizes, not a host error. ## Security Essentials The bootstrap you hand-write is security-critical. - **Serve your app from a different origin than the host.** The browser's Same-Origin Policy isolates it. In local development, different ports count as different origins. - **Trust only `hostAppOrigin`.** Verify `event.origin === hostAppOrigin` on the envelope, and send the heartbeat to that exact origin, never `"*"`. Don't maintain your own list of accepted host origins. - **Run all five envelope checks, and fail closed on a duplicate transfer.** A second valid envelope means you shut the session down and never use the second port. The worst outcome is a denied session, never a hijacked one. - **Never let the port leak.** Keep the `MessagePort` in local or module scope. Don't put it on `window`, `globalThis`, or any shared registry. - **You handle your own authentication.** The host gives you no credentials or data access, so authenticate to your own backend as usual. - **Sanitize error reports.** Send only the method name, a category, and a path-only filename. Never send a message, a stack, or a reason. ## Protocol Versions Your app declares the single version it speaks in the heartbeat (`protocolVersion: "1.0.0"`). The host accepts it by transferring the port. To refuse the session, the host doesn't transfer a port. Instead, it dispatches `sf-embedding.component.error` (code `PROTOCOL_VERSION_MISMATCH`) on its own element for the page to observe. The shipping host supports exactly 1.0.0. Pin to a version you've tested, and watch the protocol changelog. ## See Also - [Get Started with UI Embedding](https://developer.salesforce.com/docs/platform/microfrontend/guide/get-started.md) - [Track Unsaved Changes in an Embedded App](https://developer.salesforce.com/docs/platform/microfrontend/guide/track-unsaved-changes.md) - [Resize an Embedded App to Fit Its Content](https://developer.salesforce.com/docs/platform/microfrontend/guide/resize-embedded-app.md) - [UI Embedding Error Codes](https://developer.salesforce.com/docs/platform/microfrontend/references/microfrontend-error-codes/error-codes.html) # Build with Agentic Coding Tools You can build Multi-Framework apps with an agentic coding tool such as Claude Code, Cursor, OpenCode, or Agentforce Vibes. Salesforce doesn't tie Multi-Framework to one tool. Its agentic support builds on open standards, including the Agent Skills specification and the Model Context Protocol (MCP), so the same setup works across any tool that supports them. Setup comes down to three pieces: the agent context that ships with your project, a library of Salesforce skills, and an MCP server that connects your tool to your org. After that, your tool follows a standard workflow to build and deploy your app. This topic explains what each piece does and how to set it up. **note:** Keep your Salesforce CLI and Salesforce Extension Pack current so that you have the latest skills and tooling. ## Understand the Agent Context in Your Project Agentic tools read context files in your project to learn its conventions. Every generated Multi-Framework project includes an `AGENT.md` file at the project root. This file tells an agentic tool how to work in a Multi-Framework project, so any tool that reads agent-context files picks up Multi-Framework conventions automatically. You don't create or configure it. The agent context directs your tool to: - Run build, lint, and dev commands from inside the UIBundle directory (`/uiBundles//`), not the project root. - Access Salesforce data through the Data SDK rather than calling `fetch` or `axios` directly. - Leave generated files such as `schema.graphql` in place instead of editing them. - Check for a relevant skill before starting a task, because skills are the authoritative source of guidance. ## Install the AI Skills Library Skills are packaged, task-specific instructions that your agentic tool loads when it needs them. Salesforce publishes a library of skills for building on the platform, including a set specific to Multi-Framework and UIBundle development. How you install the library depends on your tool: - For Agentforce Vibes, the skills are installed and updated for you. No action is needed. - For other tools, such as Claude Code or Cursor, run: ```bash npx skills add forcedotcom/sf-skills ``` The Multi-Framework skills follow the same arc as this guide. There's a skill for generating a project, one for building the frontend, one for accessing Salesforce data, ones for creating internal and external apps, and skills for deploying, localizing, and adding features such as file upload and the Agentforce conversation client. When you ask your tool to perform one of these tasks, such as generating a project or accessing Salesforce data, it loads the matching skill. **note:** The AI skills library changes frequently and doesn't follow the same stability guarantees as generally available platform APIs. Skills can be renamed, restructured, or removed between releases. Install the library with the command above rather than depending on a fixed set of skills. ## Connect Your Tool to Your Org with the MCP Server To let your agentic tool authorize orgs, deploy and retrieve metadata, run queries, and fetch your GraphQL schema, connect it to the Salesforce DX MCP server. You add an MCP server to your tool's configuration rather than running it directly. Where that configuration lives depends on your tool, but each tool needs the same command and arguments: - Command: `npx` - Arguments: `-y @salesforce/mcp --orgs DEFAULT_TARGET_ORG --toolsets orgs,metadata,data` Most tools express this as a JSON entry: ```json { "mcpServers": { "salesforce": { "command": "npx", "args": [ "-y", "@salesforce/mcp", "--orgs", "DEFAULT_TARGET_ORG", "--toolsets", "orgs,metadata,data" ] } } } ``` The toolsets most relevant to Multi-Framework development are: - `orgs`: authorize, open, and manage orgs. - `metadata`: deploy and retrieve metadata, including your UIBundle. - `data`: run SOQL queries against your org. The MCP server authorizes an org you've already connected with the Salesforce CLI. Authorize your org before you start your tool. ## How Your Tool Builds the App With skills and the MCP server in place, your agentic tool follows the workflow defined in the project's agent context: 1. It works from the UIBundle directory and uses the project's `npm` scripts to build, lint, and run the dev server. 2. It fetches the GraphQL schema from your org with `npm run graphql:schema`, then generates typed operations from it with `npm run graphql:codegen`. 3. It accesses Salesforce data through the Data SDK. See [Access Salesforce Data and Platform Capabilities](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk/data-sdk-usage.md). 4. It deploys the UIBundle and its metadata to your authorized org through the MCP server. ## Related Salesforce Building Tools The tools in this topic are for pro-code developers who build Multi-Framework apps in code. Salesforce also offers Builder Central (Beta), a no-code way for admins and builders to create apps and agents by describing what they want in plain language. Builder Central builds React-based apps, but through a guided, conversational flow rather than in code. ## See Also - [Set Up Your Org for Multi-Framework App Development](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-setup.md) - [Quick Start: Build and Run Your First App](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-quick-start.md) - _AI Skills Library_: [forcedotcom/sf-skills](https://github.com/forcedotcom/sf-skills) - _Salesforce Help_: [Get Started with Builder Central (Beta)](https://help.salesforce.com/s/articleView?id=platform.builder_central_get_started.htm&type=5) # Cache Control in Data SDK The `cacheControl` parameter on the GraphQL `query` method overrides the default caching behavior for individual queries. Cache control is available on web apps and it's ignored on uncached surfaces. Valid `cacheControl` values include: - `no-cache`: Stores a response but requires revalidation before reuse. - `only-if-cached`: Returns a stored response, or a `DataNotFoundError` on a cache miss. **note:** `cacheControl` doesn't affect the cache key. Within a single cache store, the per-entry cache key is `{query, variables, operationName, headers}`. Two calls that are identical except for their [per-request `headers`](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-request-headers.md) resolve to different cache entries to ensure that a response scoped by a header, such as `Authorization`, isn't served to a later caller that passed a different value. Calls with equivalent headers, including calls that pass no `headers` at all, share an entry. The cache store itself is partitioned by base URL and API version, so SDK instances that target different API versions never share cache entries, even for otherwise identical queries. See [Work with Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-intro.md#createdatasdkoptions). ## Default Behavior (300s TTL) If a `cacheControl` value isn't provided, the SDK applies a default `max-age` strategy with a 300-second (5-minute) time to live (TTL). After the `max-age` expires, the next call fetches fresh data from the network. A stale entry is treated as a cache miss. ```typescript const result = await dataSdk.graphql?.query({ query: GET_ACCOUNTS, variables: { first: 20 }, }); ``` The default caching behavior covers these scenarios. - Cache hit: Cached responses that are less than 300 seconds are returned immediately. - Stale cache: Responses that are more than 300 seconds are considered stale; the SDK fetches fresh data from the network. - Cache miss: The SDK fetches data from the network and stores the response in the cache with a 300-second TTL. ## `cacheControl: "no-cache"` To skip the cache and always fetch fresh data from the server, use `cacheControl: "no-cache"`. ```typescript const result = await sdk.graphql?.query({ query: GET_ACCOUNTS, variables: { first: 20 }, cacheControl: "no-cache", }); ``` When you use `no-cache`, the SDK: - Writes the response back to cache (with 300s TTL) for subsequent default callers - Revalidates the cached response with the origin server before reusing it Use `no-cache` when you want to trigger a force-refresh or reload on a page, such as after a known mutation operation. ## `cacheControl: "only-if-cached"` To read from the cache only, use `cacheControl: "only-if-cached"`. The SDK reads from the cache only and never hits the network, even for a cache miss situation. ```typescript const result = await sdk.graphql?.query({ query: GET_ACCOUNTS, variables: { first: 20 }, cacheControl: "only-if-cached", }); ``` When you use `only-if-cached`, the caching behavior covers these scenarios. - Cache hit: Returns cached data - Cache miss: Returns error (`DataNotFoundError`) A cache miss isn't thrown. The Promise resolves and the cache miss surfaces on `result.errors`, carrying a [typed discriminator](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-graphql-error.md) so you can tell it apart from a server-returned error. Use `only-if-cached` when you know the data was already fetched. `only-if-cached` is also useful in offline-first patterns, avoiding loading states when stale data is acceptable. ## `cacheControl: { type: "max-age", maxAge: 60 }` To change the default 300s TTL value, pass in your own `maxAge` value in seconds with `type: "max-age"`. ```typescript const result = await sdk.graphql?.query({ query: GET_ACCOUNTS, variables: { first: 20 }, cacheControl: { type: "max-age", maxAge: 60 }, }); ``` When you pass in your own `maxAge` value, the caching behavior looks like this. - Same as the default behavior, but the data goes stale after the `maxAge` value you provide, instead of the default 300s TTL value - Writes to cache with the `maxAge` value you provide Use this pattern when your data changes frequently, such as in dashboards or notifications. **note:** `maxAge` must be a finite non-negative number. Invalid values such as negative numbers and `NaN` fall back to 300s silently. `maxAge: 0` is valid and means the cache is always stale, resulting in a re-fetch on every call. ## See Also - [GraphQL Queries in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-graphql.md) - [GraphQL Mutations in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-graphql-mutation.md) - [Error Handling in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-graphql-error.md) # Create a Data SDK Extension Package your own reusable data logic behind a typed API on the SDK instance by authoring a custom extension. With an extension, you can: - Wrap repeated GraphQL queries, a caching layer, a custom API client, telemetry, and so on into one reusable namespace instead of duplicating them. - Get full type safety via `sdk.ext` for each extension's return type. Duplicate names or non-literal names are rejected at compile time. - Isolate your capabilities from SDK evolution under the `sdk.ext` namespace, so you get future SDK changes easily. ## Define an Extension Define a custom extension with `defineDataExtension`. Provide a name for your extension, and a `setup` function that receives the resolved `DataSDK` instance. `setup` can be synchronous or asynchronous, so you can `await` a dynamic import or a GraphQL query before you return the API. ```typescript import { createDataSDK } from "@salesforce/platform-sdk/data"; import { defineDataExtension, type DataSDK } from "@salesforce/platform-sdk"; interface AccountApi { getName?(recordId: string): Promise; } // The payload shape of the GraphQL query, as it appears under `data`. interface AccountNameResult { uiapi: { query: { Account: { edges: { node: { Name: { value: string } } }[] } } }; } const accountExtension = defineDataExtension({ name: "account", setup: (sdk: DataSDK): AccountApi => { // GraphQL isn't available on every surface, so check for it during setup. // Omit getName when it isn't supported instead of returning undefined at call time. const graphql = sdk.graphql; if (!graphql) return {}; return { async getName(recordId) { const result = await graphql.query({ query: GET_ACCOUNT_NAME, variables: { recordId }, }); return result?.data?.uiapi.query.Account.edges[0]?.node.Name.value; }, }; }, }); // Attach the extension, then call it under sdk.ext. // getName is optional, so guard the call: it's absent when GraphQL isn't supported. const sdk = await createDataSDK({ extensions: [accountExtension] }); const name = await sdk.ext.account.getName?.("001xx000003DGb2AAG"); ``` Pass the `extensions` array inline, as in the example, so that the SDK types `sdk.ext.` for each extension you attach. If you assign the array to a variable first, add `as const` to keep the `sdk.ext` types. ## defineDataExtension Fields | Field | Type | Description | | :-------------- | :-------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | The name to refer to the extension using the `sdk.ext.` format. | | `setup` | `(sdk: DataSDK) => Api \| Promise` | Builds the extension API. Runs when you create the SDK instance, with the resolved `DataSDK` passed in. Returns the API object under `name`. | | `minApiVersion` | `string` | Optional. The earliest Salesforce API version the extension supports, such as `"64.0"`. Specify an API version when the extension depends on a data shape or capability from a specific API version. See [Set a Minimum API Version](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-intro.md#set-a-minimum-api-version). | ## Attach an Extension Attach extensions by passing them to `createDataSDK()` in the `extensions` array. The SDK builds each extension's API when you create the instance, so the extensions are ready to use with `createDataSDK()`. Call an extension's API under the SDK's `ext` namespace using `sdk.ext.`. ```typescript import { createDataSDK } from "@salesforce/platform-sdk/data"; import { labels } from "@salesforce/platform-sdk/data/extensions"; const sdk = await createDataSDK({ extensions: [labels({ namespace: "MyApp" })] }); // Call the extension API under sdk.ext const greeting = await sdk.ext.labels.get?.("Greeting_Label"); ``` Pass the `extensions` array inline, as in the example, so that the SDK types `sdk.ext.` for each extension you attach. If you assign the array to a variable first, add `as const` to keep the `sdk.ext` types. ## Specify Unique API Names Add each extension using a unique API name. If two extensions use the same `name`, attach one with an alias so each has a distinct key. Pass `{ extension, as }` instead of the bare extension, and call the aliased extension under its `as` key. ```typescript const sdk = await createDataSDK({ extensions: [labels({ namespace: "MyApp" }), { extension: partnerLabels, as: "partnerLabels" }], }); sdk.ext.labels.get?.("Greeting_Label"); sdk.ext.partnerLabels.get?.("Partner_Greeting"); ``` ## Set a Minimum API Version An extension can declare a `minApiVersion`, the earliest Salesforce API version it supports. `minApiVersion` indicates whether the server operations the extension uses are supported at the resolved Salesforce API version. It's distinct from runtime capability checks, which determine whether functionality such as `graphql` or `fetch` is available on the surface. When an extension declares `minApiVersion` and the runtime supplies an API version, `createDataSDK()` checks the `minApiVersion` against the API version the SDK resolves for the surface. If the resolved version is lower than the extension's `minApiVersion`, `createDataSDK()` rejects with an error and doesn't attach any extension in the array. If the runtime doesn't supply an API version, `createDataSDK()` skips the comparison. ```typescript const sdk = await createDataSDK({ extensions: [labels()], // requires API version 64.0 or later }); ``` ## See Also - [Data SDK Extensions](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-extension.md) - [Get Custom Labels with the Labels Extension](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-extension-label.md) - [Work with Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-intro.md) - [GraphQL Queries in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-graphql.md) # Data SDK Extensions A Data SDK extension adds a self-contained, named API that gives the SDK instance extra capabilities. Use an extension that comes with the SDK, such as for retrieving a custom label from your org. Or create an extension to package your own reusable code. - To read custom labels with the `labels` extension, see [Get Custom Labels](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-extension-label.md). - To package your own reusable data logic behind a typed API, see [Create a Data SDK Extension](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-extension-custom.md). Extensions play a role similar to `@salesforce/*` scoped modules in Lightning Web Components (LWC), but they work differently. - A scoped module is an import that the LWC compiler resolves when it builds your component, so its capability is fixed into the component at build time. - A Data SDK extension is an opt-in capability that you add when your app runs; you pass it to `createDataSDK()`, and its API becomes available on the SDK instance as `sdk.ext.`. Because you add extensions when you create the SDK, you choose which ones each SDK instance includes. You can also package and share your own extensions as regular code, without depending on a special module that the compiler has to recognize. ## See Also - [Work with Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-intro.md) # Deploy and Publish a UIBundle After you build and preview your app, deploy it to your Salesforce org as metadata and publish a version so that users can access it. Because your app is a UIBundle—a Salesforce metadata type—you deploy it with the same Salesforce DX workflows and CLI commands that you use for any other source, and you can include it in your deployment pipeline. This topic applies to both React and Angular apps. Build your production assets with your framework's build command first (for example, `npm run build`, which runs `vite build` for React or `ng build` for Angular), and then deploy the resulting UIBundle. ## Before You Deploy Confirm that: - Your app builds cleanly. Run your framework's build command from the UI bundle directory before you deploy, and verify that it produces the output directory named in `ui-bundle.json` (`dist` by default). Deploy the built assets, not just your source. - The `apiVersion` in `ui-bundle.json` matches the API version of the target org. If you omit `apiVersion`, the app defaults to the org's current version. - Your target org is configured for Multi-Framework development. See [Set Up Your Org for Multi-Framework App Development](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-setup.md). - For an external app, the Experience Cloud site metadata (DigitalExperience, DigitalExperienceConfig, Network, CustomSite) is present in your project. See [Project Structure and Metadata](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-project-structure.md). ## Deploy the UIBundle Deploy your app the same way you deploy any other metadata, with `sf project deploy start`. To deploy a single UIBundle to your default org: ```shell sf project deploy start --source-dir force-app/main/default/uiBundles/MyReactApp ``` To deploy the whole project, including the CustomApplication or Experience Cloud site metadata that the app depends on: ```shell sf project deploy start --source-dir force-app ``` An internal app also needs its CustomApplication metadata, and an external app needs its site metadata, to appear in the UI. Deploy those types together with the UIBundle—for example, by deploying the whole `force-app` directory—so the app resolves correctly. Salesforce DX resolves the dependencies, so no special metadata-type order or manifest is required. See [Internal and External App Files](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-project-structure.md#internal-and-external-app-files). ## Versioning and Publication Lifecycle Each UIBundle carries a `version` in its `.uibundle-meta.xml` file and an `isActive` flag. ```xml myapp A Salesforce UI Bundle. true 1 CustomApplication ``` - **`version`**: A positive integer that identifies the schema version of the UIBundle definition. New apps start at `1`. - **`isActive`**: Controls whether the deployed UIBundle is served to users. Set it to `true` to serve the app. To update a deployed app, rebuild your production assets and redeploy the UIBundle. Salesforce stores your app's files and serves the active version, so you manage the publication lifecycle on the platform rather than on an external web server. ## Verify the Deployed App After deployment: - **Internal app**: Open the app from the App Launcher on desktop or in the Salesforce mobile app. If the app doesn't appear, confirm that the UIBundle `target` is `CustomApplication`, that the CustomApplication metadata references the UIBundle, and that users have access. See [Manage Your App and Grant User Access](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-manage.md). - **External app**: Open the site from the Digital Experiences app or the Experience Cloud site URL. ## Include Your App in a Deployment Pipeline Because the UIBundle is source-format metadata in your Salesforce DX project, you can commit it to version control and deploy it through your existing CI/CD pipeline alongside your other metadata. Build the production assets as a pipeline step before the deploy, and treat the UIBundle like any other component in your package. ## See Also - _Salesforce CLI Command Reference_: [project deploy start](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference_project_commands_unified.htm) - _Metadata API Developer Guide_: [UIBundle](https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_uibundle.htm) - [Multi-Framework recipes repo](https://github.com/trailheadapps/multiframework-recipes) # Embed an External Web App (Developer Preview) Embed an externally hosted web app inside a Salesforce page with the `` host component. Salesforce loads your app in a sandboxed iframe and manages the connection between the page and your app. **note:** This topic describes the Developer Preview API (``, package `@salesforce/experimental-mfe-lwc-shell`). The generally available release replaces this with the `` component and a different SDK. Treat every API name in this topic as Developer Preview only. To move an app to the generally available version, see [Migrate from the Developer Preview API](https://developer.salesforce.com/docs/platform/microfrontend/guide/migrate-from-dev-preview.md). ## How It Works Salesforce embeds your externally hosted app in an iframe managed by the `` custom element. You generate a deployable Lightning web component from the shell package, wrap it in your own component, point it at your app's URL, and add it to the page. The shell applies a sandbox policy to the iframe and opens a message channel to your app. ## Prerequisites - An externally hosted web app served over HTTPS. - The `@salesforce/experimental-mfe-lwc-shell` package (Developer Preview). - Permission to create Lightning web components and edit Lightning pages in your org. ## Generate the Deployable Component Run the vendor build to generate a deployable Lightning web component from the shell package. ```bash npx lwc-shell-vendor-build ``` This creates a component at `force-app/main/default/lwc/vendorLwcShell/` that you can deploy and reference from your wrapper component. ## Create the Shell in a Wrapper Component Create the shell imperatively, and set its attributes before you add it to the DOM. ```javascript const shell = document.createElement("lwc-shell"); shell.title = "Loan application"; shell.src = appUrl; shell.sandbox = "allow-forms allow-modals"; container.appendChild(shell); ``` **important:** Create the shell with `document.createElement('lwc-shell')` and set `src`, `title`, and any `sandbox` tokens before you call `appendChild`. A declarative `` tag placed directly in an LWC template doesn't upgrade to the custom element, so the imperative pattern is required. ## Sandbox Policy The shell applies a tiered iframe sandbox policy: - **Base tokens**, always applied: `allow-scripts`, `allow-pointer-lock`, `allow-same-origin`. - **Optional tokens**, which you opt into through the `sandbox` attribute: `allow-forms`, `allow-modals`, `allow-downloads`. - **Blocked tokens**, which the host removes even if you request them: `allow-top-navigation`, `allow-popups`. The host blocks `allow-top-navigation` and `allow-popups` so an embedded app can't navigate the top-level window or escape the sandbox through a popup. **warning:** `allow-same-origin` for a third-party embedded app works only on Experience Cloud sites that have Locker or Lightning Web Security disabled. It doesn't work on Lightning Experience pages in Developer Preview. Cookies and `localStorage` in the embedded app depend on `allow-same-origin`, so they're subject to the same limitation. This limitation is resolved in the generally available release, where the shell becomes a first-party component. ## Allowlist Your App's Domain Before Salesforce can load your app, add your app's domain to the org's allowlist in Setup. ## See Also - [Exchange Data Between the Page and an Embedded App (Developer Preview)](https://developer.salesforce.com/docs/platform/microfrontend/guide/dp-exchange-data.md) - [Match an Embedded App to the Salesforce Theme (Developer Preview)](https://developer.salesforce.com/docs/platform/microfrontend/guide/dp-style-embedded-app.md) # Embed an Internally Hosted App as a UI Bundle (Beta) Host your app inside Salesforce infrastructure instead of on your own domain, and embed it with the same `lightning-ui-embedding` component you use for external apps. An internally hosted app is packaged as a UI Bundle, deployed to your org, and served from the Salesforce-managed `*.salesforce.app` domain. **note:** _Embedding internally hosted UI Bundles is a pilot or beta service that is subject to the Beta Services Terms at [Agreements - Salesforce.com](https://www.salesforce.com/company/legal/agreements/) or a written Unified Pilot Agreement if executed by Customer, and applicable terms in the [Product Terms Directory](https://ptd.salesforce.com/). Use of this pilot or beta service is at the Customer's sole discretion._ Some capabilities described elsewhere for UI Embedding aren't available for internally hosted bundles in this release, and embedding a bundle currently requires manual Trusted URL and CSP configuration. See the prerequisites and limitations below. ## How It Works An internally hosted UI Bundle is a React app that you deploy to your org. Salesforce serves it from its own `*.salesforce.app` domain. Instead of pointing the embedding component at an external URL, you reference the bundle by its explicit full `*.salesforce.app` URL, and Salesforce embeds it through the same `sf-embedding` protocol used for externally hosted apps. Before you embed a bundle, confirm it runs on its own. Open the deployed app from the App Launcher and copy its `*.salesforce.app` URL from the browser address bar. That's the URL you hand to your wrapper component. ## Prerequisites - You have a React UI Bundle deployed to your org. - You can create Lightning web components and edit Lightning pages or Experience Cloud sites in your org. - The UI Bundle launches on its own from the App Launcher. If it doesn't load, enable Salesforce Edge Network in your My Domain settings. ## Configure Trusted URLs and CSP Because the bundle loads in an iframe from a different domain, you allow that domain (and the domains it redirects through to establish a session) in your org's Content Security Policy. The settings depend on where you embed the bundle. ### Lightning Experience pages - In Setup, go to Trusted URLs and add `*.salesforce.app` with the `frame-src` directive. ### Experience Cloud sites (LWR-based and Aura-based) Embedding on an Experience Cloud site requires more settings, because the bundle's session is established through a chain of Salesforce domains inside the iframe. 1. In Setup, go to Trusted URLs and add these three URLs with the `frame-src` directive: - `*.salesforce.app` - Your org's Lightning domain (for example, `*.lightning.force.com`) - Your My Domain URL (for example, `*.my.salesforce.com`) 2. In Session Settings, under Trusted Domains for Inline Frames, add your Experience Cloud site URL with the iframe type **UIEmbedding**. This allows the site to frame the bundle. 3. In My Domain, under Routing and Policies, turn off **Require first-party use of Salesforce cookies**. The bundle relies on Salesforce cookies to establish its session inside the iframe, and this setting blocks those cookies in a framed (third-party) context. ## Reference the UI Bundle by URL In your wrapper component, set the embedding component's `src` to the bundle's full explicit `*.salesforce.app` URL, the same way you point it at an external app's URL. ## Limitations in This Release - **React only.** As of now, React UI Bundles are generally available; Angular and Vue support is coming next. - **Explicit URL only.** Dynamic URL resolution and packaging aren't available. You reference the bundle by its full `*.salesforce.app` URL. - **App Builder supports Lightning web components only.** You can't drag React, Angular, or Vue UI Bundles onto a page in App Builder in this release. - **Manual CSP configuration.** The Trusted URL and CSP settings above are configured manually in this release. Salesforce plans to handle them automatically in a later release. ## See Also - [Get Started with UI Embedding](https://developer.salesforce.com/docs/platform/microfrontend/guide/get-started.md) - [Build Embedded Apps in the Framework You Prefer](https://developer.salesforce.com/docs/platform/microfrontend/guide/framework-support.md) # Embed Your App in Salesforce with Microfrontends (Beta) You can embed a Multi-Framework app directly in Salesforce so your users work in one place instead of switching to a separate tab or window. This pattern of running your app as a component inside a Salesforce page is called a microfrontend. On the platform, you build it with UI Embedding. Salesforce hosts your app inside a secure, sandboxed iframe using the `lightning-ui-embedding` base component, and gives your app a typed channel to exchange styling, sizing, and lifecycle signals with the surrounding page. To embed an app, write a thin Lightning web component (LWC) that renders `lightning-ui-embedding` and points its `src` at your app's URL. Then add that component to a Lightning page or an Experience Cloud site page. The app you build is a deployed UIBundle, which Salesforce hosts on the `*.salesforce.app` domain. To embed it, point the embedding component at the bundle's `*.salesforce.app` URL, which is the same URL the app opens from the App Launcher. ## Before You Embed Your App To embed your UIBundle, first get it running as a standalone app: 1. Deploy your UIBundle and grant access. See [Deploy and Publish a UIBundle](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-deploy.md) and [Manage Your App and Grant User Access](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-manage.md). 2. Open the app from the App Launcher and copy its full `*.salesforce.app` URL from the browser address bar. That's the URL you give the embedding component. ## Learn More Learn more about microfrontends, including building the wrapper component, handling error states, and using the view SDK. - _Salesforce Microfrontend (UI Embedding)_: [Get Started with UI Embedding](https://developer.salesforce.com/docs/platform/microfrontend/guide/get-started.html) - _Salesforce Microfrontend (UI Embedding)_: [Embed an Internally Hosted App as a UI Bundle](https://developer.salesforce.com/docs/platform/microfrontend/guide/internal-ui-bundles.html) # Error Handling in Data SDK GraphQL can return both data and errors in the same response for queries or mutation operations that result in partial success. For example, the record operation succeeds, but some return fields aren't accessible due to the user's permission. Follow these error-handling strategies. ## Strict: Treat Any Errors as Failure This strategy ensures a complete failure if any GraphQL errors are present in the response, regardless of whether partial data was also returned. As the most conservative approach, it guarantees that the app processes data only when the request was entirely successful. Use this strategy with queries where incomplete data is misleading. ```javascript if (result?.errors?.length) { throw new Error(result.errors.map((e) => e.message).join("; ")); } const data = result?.data; ``` ## Tolerant: Log Errors but Use Partial Data Data SDK preserves partial data from GraphQL error responses. When the server returns both `data` and `errors`, both fields are available in the result. This strategy allows you to gracefully degrade the UI by rendering the fetched data while logging errors for fields that failed. **note:** Partial success responses where both `data` and `errors` are present aren't cached. Each subsequent query re-fetches data from the network. This strategy is suitable for cases where partial data is still usable by the app, even if the GraphQL request returns errors. The errors are logged for review, but the app continues to process the available data. Use this strategy when partial data is still useful and the UI can degrade gracefully, such as with an error on an optional field. ```javascript const result = (await dataSdk.graphql?.query) < AccountData > { query: GET_ACCOUNT, variables: { id: accountId }, }; if (result?.errors?.length) { console.warn("GraphQL partial errors:", result.errors); } // result.data may contain partial data even when errors are present const data = result?.data; ``` ## Fail Only When No Data is Returned This is the least restrictive strategy. It permits the app to use any data that was successfully retrieved, even if partial errors occurred during the GraphQL request. The app throws an error only and fails if the response contains errors and no usable data was returned. For example, you can use this strategy in mutations where the operations succeed but some return fields aren't accessible. ```javascript // Fail only when no data is returned if (result?.errors?.length && !result?.data) { throw new Error(result.errors.map((e) => e.message).join("; ")); } if (result?.errors?.length) { console.warn("Partial success with errors:", result.errors); } const data = result?.data; ``` ## Network and Runtime Transport Errors When handling `fetch()` errors for the Data SDK, consider transport and runtime errors where no responses are returned. For example, transport errors can include network failures, aborted requests, or runtime exceptions. Wrap transport errors and check the response. ```typescript const dataSdk = await createDataSDK(); let response: Response | undefined; try { response = await dataSdk.fetch?.("/services/apexrest/data-access/account-summary", { method: "GET", headers: { Accept: "application/json" }, }); } catch (error) { throw new Error(`Network/transport failure: ${(error as Error).message}`); } if (!response?.ok) { throw new Error(`Request failed: ${response?.status} ${response?.statusText}`); } const data = await response.json(); ``` ## HTTP Errors When handling fetch() errors for the Data SDK, you can run into HTTP app errors with a non-2XX response that includes an error payload. For auth-sensitive flows, consider handling 401 and 403 errors with dedicated user actions, such as a sign-in refresh or a permission guidance message. ```typescript const dataSdk = await createDataSDK(); const response = await dataSdk.fetch?.("/services/apexrest/auth/login", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify(payload), }); if (response.status === 401) { throw new Error("Authentication required or session expired."); } if (response.status === 403) { throw new Error("You do not have permission to perform this action."); } if (!response.ok) { throw new Error(`Request failed: ${response.status} ${response.statusText}`); } ``` ## Extensions Errors Each entry in `result.errors` is a `GraphQLError` that can carry an `extensions` object. When present, `extensions.code` provides a stable value to branch on for a specific error, rather than matching on `message` text. ```typescript const result = await sdk.graphql?.query({ query: GET_ACCOUNTS, variables: { first: 20 }, cacheControl: "only-if-cached", }); if (result?.errors?.[0]?.extensions?.code === "CACHE_MISS") { // Cold cache — fall back to a network fetch or render an empty state. } else if (result?.errors?.length) { // A different failure — handle it through your normal error path. } ``` The SDK sets `extensions.code === "CACHE_MISS"` when a request using `only-if-cached` finds nothing in the cache, so you can tell an empty cache apart from a real failure. Check the code first, then handle anything else with your usual error path. For more information, see [Cache Control in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-cache-control.md). ## Example: Partial Success in Update This example shows how to handle partial success in an update operation. ```typescript async function updateAccount(id: string, updates: Partial) { const dataSdk = await createDataSDK(); const result = await dataSdk.graphql?.mutate({ mutation: UPDATE_ACCOUNT, variables: { input: { Id: id, Account: updates, }, }, }); // Check if the mutation succeeded if (result?.errors?.length && !result?.data) { // Total failure - mutation didn't execute throw new Error(result.errors.map((e) => e.message).join("; ")); } // Partial success - mutation executed but some fields had errors if (result?.errors?.length) { console.warn("Account updated with warnings:", result.errors); // You can still use result.data } return result?.data?.uiapi?.AccountUpdate?.Record; } ``` ## Error Handling in LWC and React The approach to error handling in LWC and other frameworks such as React differs due to their distinct architectural models. - LWC: In addition to standard JavaScript error handling patterns, LWC provides an `errorCallback(error, stack)` lifecycle method in a parent component to catch errors from its children. When making @wire calls, errors are returned in an error object, which standardizes error handling in LWC. - React: Error handling typically relies on standard JavaScript mechanisms like promises for asynchronous Data SDK calls. You can also use error boundaries, which are special components (`componentDidCatch`, `getDerivedStateFromError`) that catch JavaScript errors in the component tree and display a fallback UI. **tip:** The error handling examples in the [Multi-framework recipes repo](https://github.com/trailheadapps/multiframework-recipes) demonstrate how to use error boundaries, handle GraphQL errors, and handle the loading, error, and empty states. ## See Also - [GraphQL Queries in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-graphql.md) - [GraphQL Mutations in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-graphql-mutation.md) - [Cache Control in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-cache-control.md) # Exchange Data Between the Page and an Embedded App Move data both ways between Salesforce and an embedded app so the two stay in sync. The Salesforce host sends the embedded app its current data and UI state, and the embedded app sends values and events back to the host. Both directions travel over the same versioned `sf-embedding` channel, so you don't manage `postMessage` calls or origins yourself. ## How It Works Salesforce composes a UI state for each embedded app and pushes it to the guest over the embedding protocol. The UI state carries the host-driven data your app reads, such as the `props` the page passes in and the `styles` it applies. Your app reads the current snapshot and subscribes to receive later snapshots when the data changes. Snapshots are full replacements, not diffs. In the other direction, your app sends data and custom events back to the host. Events surface on the host `lightning-ui-embedding` element, where a Lightning web component on the page can listen for them. Every value that crosses the channel travels by structured clone. **important:** Values that cross the channel must be structured-clone-safe. Structured clone preserves `Date`, `Map`, `Set`, and typed arrays, so a `Date` arrives as a `Date`, not as an ISO string. Don't `JSON.stringify` a payload before you send it, and don't send values that can't be cloned, such as functions or DOM nodes. ## Prerequisites - Your app is embedded with `lightning-ui-embedding` and loads successfully. - Your app can read the platform SDK, or you build the embedding bridge yourself. See [Get Started with UI Embedding](https://developer.salesforce.com/docs/platform/microfrontend/guide/get-started.md) for the two configuration paths. ## Read Host Data in the Embedded App 1. In your embedded app, create the view SDK and read the current UI state. `createViewSDK` is asynchronous, so await it. `getUiState` returns the current state and a `subscribe` function you use to react to later updates. ```javascript import "@salesforce/platform-sdk/ui-embedding"; import { createViewSDK } from "@salesforce/platform-sdk/view"; const viewSdk = await createViewSDK(); // Off an embedding surface, createViewSDK resolves to an empty object, // so guard the call before you use it. const uiState = viewSdk.getUiState?.(); if (uiState) { render(uiState.state.props); } ``` 2. Read the host-driven data from `state.props` and use it in your app. 3. Subscribe so your app updates when the host data changes. Salesforce pushes a new snapshot on every change, so reapply the values each time. ```javascript const unsubscribe = uiState.subscribe((next) => { render(next.props); }); ``` **note:** There's at most one active UI-state subscription per session, and each snapshot is a full replacement of the previous one. To match the host's styling from the same UI state, see [Style an Embedded App to Match Salesforce](https://developer.salesforce.com/docs/platform/microfrontend/guide/style-embedded-app.md). ## Send Data and Events Back to the Host Your app sends a custom event to the host, and a Lightning web component on the page listens for it. Use this to notify the page when a user selects a record, completes a step, or changes a value the page needs. The event surfaces on the host `lightning-ui-embedding` element as a DOM event that carries your event type and detail. Add a listener to your wrapper component to receive it. **note:** Because the embedding event names contain dots, bind them imperatively with `addEventListener` rather than declaratively with `on*` template attributes, the same way you handle the `sf-embedding.component.ready` and `sf-embedding.component.error` events in [Get Started with UI Embedding](https://developer.salesforce.com/docs/platform/microfrontend/guide/get-started.md). To build the data exchange yourself without a Salesforce package, send and receive the underlying `ui/events/dispatch` and `ui/subscribe/ui-state` messages directly over the protocol. See [Build the Embedding Bridge Without a Salesforce Package](https://developer.salesforce.com/docs/platform/microfrontend/guide/package-free-bridge.md). ## Show the Embedded App in the User's Language Your app controls its own UI, so localizing that UI is your app's job. To render in the same language as Salesforce, your app needs the host's locale. Pass it from your wrapper component to the app over the same custom-event channel you use to exchange data. In your wrapper component, import the Salesforce locale and dispatch it to the embedding element as a custom event. Pick an event name that your app listens for, such as `localeInformation`. Add `lwc:ref` to the embedding element the same way you do to listen for its lifecycle events. Send the locale after the embedding component reports that it's ready, so the app has it before it renders. ```javascript // myEmbeddedApp.js import { LightningElement } from "lwc"; import locale from "@salesforce/i18n/locale"; export default class MyEmbeddedApp extends LightningElement { appUrl = "https://app.example.com"; sendLocale() { this.refs.embedding.dispatchEvent(new CustomEvent("localeInformation", { detail: { locale } })); } } ``` In your embedded app, listen for that same event, read the locale from its detail, and render your UI in that language. **note:** Passing the locale tells your app which language to render in. Translating the app's own content, labels, and number and date formats stays your app's responsibility. The Salesforce unsaved-changes modal is already shown in the user's language. ## See Also - [Get Started with UI Embedding](https://developer.salesforce.com/docs/platform/microfrontend/guide/get-started.md) - [Style an Embedded App to Match Salesforce](https://developer.salesforce.com/docs/platform/microfrontend/guide/style-embedded-app.md) - [Build the Embedding Bridge Without a Salesforce Package](https://developer.salesforce.com/docs/platform/microfrontend/guide/package-free-bridge.md) # Exchange Data Between the Page and an Embedded App (Developer Preview) Pass data from the Salesforce page to your embedded app, and respond in the page to events your app raises. The `` host and the `@salesforce/experimental-mfe-bridge` running inside your app exchange messages across the iframe boundary. **note:** This topic describes the Developer Preview API. The host method `shell.updateData()` and the `@salesforce/experimental-mfe-bridge` package are Developer Preview only. In the generally available release, data exchange moves to a reactive UI-state model rather than an imperative method call, so don't build long-term code around `updateData()`. To move an app to the generally available version, see [Migrate from the Developer Preview API](https://developer.salesforce.com/docs/platform/microfrontend/guide/migrate-from-dev-preview.md). ## How It Works The page sends data to your app by calling a method on the shell. Your app receives that data through the bridge. When your app raises an event, the bridge forwards it across the iframe boundary, and the shell re-fires it in the page so your wrapper component can respond. ## Send Data to the Embedded App Call `updateData` on the shell with the data your app needs. ```javascript shell.updateData({ recordId: "001xx000003DGb2AAG", locale: "en_US" }); ``` If you call `updateData` before the app's bridge has finished connecting, the shell holds the data and delivers it once the connection is ready. You don't have to wait for a ready signal before sending the first payload. **note:** Data crosses the iframe boundary through the browser's messaging channel. Pass values that survive that boundary. Plain objects, strings, numbers, arrays, and booleans are safe. Values that can't be cloned, such as functions, cause an error. ## Receive Data in the Embedded App Inside your app, read data from the bridge as it arrives. ```javascript import { bridge } from "@salesforce/experimental-mfe-bridge"; bridge.addEventListener("data", () => { const data = bridge.getData(); // use data.recordId, data.locale, and so on }); ``` ## Respond to Events from the Embedded App When your app dispatches an event through the bridge, the shell re-fires it in the page. Subscribe with `addEventListener` on the shell element. ```javascript shell.addEventListener("save-complete", (event) => { // handle event.detail }); ``` ## See Also - [Embed an External Web App (Developer Preview)](https://developer.salesforce.com/docs/platform/microfrontend/guide/dp-embed-external-web-app.md) - [Match an Embedded App to the Salesforce Theme (Developer Preview)](https://developer.salesforce.com/docs/platform/microfrontend/guide/dp-style-embedded-app.md) # Generate an App from a Template Salesforce Multi-Framework templates scaffold a fully configured project so you can focus on building features instead of configuring plumbing. The templates include the UIBundle metadata, runtime configuration, framework tooling, and the additional metadata your app needs to run on the Salesforce Platform. You generate an app with the Salesforce CLI, using the same `sf` commands you use for other Salesforce metadata. ## Choose a Template Choose a template based on your framework and your app's audience. Internal templates target employees who sign in with Salesforce credentials, whereas external templates target customers, partners, or public users who reach your app through an Experience Cloud site. An internal app and an external app use different project templates and platform configuration, and require some additional metadata types to integrate with the Salesforce Platform. | Feature | Internal Apps | External Apps | | ---------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | **Audience** | Employees with Salesforce logins | Customers, partners, or public users | | **Authentication** | Salesforce credentials (SSO supported) | Experience Cloud guest access or authenticated login | | **Access point** | App Launcher on desktop and the Salesforce mobile app | Digital Experiences app and Experience Cloud site URL | | **Template** | `reactinternalapp`, `angularinternalapp` | `reactexternalapp`, `angularexternalapp` | | **Metadata Types** | UIBundle + CustomApplication | UIBundle + Experience Cloud site metadata (DigitalExperience, DigitalExperienceConfig, Network, CustomSite) | | **Pre-built features** | Agentforce Conversation Client, object search | Navigation, authentication, object search | For more information, see [Project Structure and Metadata](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-project-structure.md). ## Generate a New Project To start a new app, you can generate a Salesforce DX project from an internal or external template. The internal and external app templates scaffold a full project, so you generate them with `sf template generate project`. The generated project includes the UIBundle and all of the additional metadata the app needs to run. For example, for an internal React app, use: ```shell sf template generate project --name MyReactApp --template reactinternalapp cd MyReactApp ``` Or for an external Angular app, use: ```shell sf template generate project --name MyAngularApp --template angularexternalapp cd MyAngularApp ``` ## Add a UI Bundle to an Existing Project If you already have a Salesforce DX project and want to add an app to it, generate a UI bundle into the existing project rather than scaffolding a new one. Use a bundle-only template in this case, namaly `reactbasic` for React or `angularbasic` for Angular. ```shell sf template generate ui-bundle --name MyApp --template reactbasic ``` The command adds the app under `force-app/main/default/uiBundles/`. You supply any additional metadata, such as CustomApplication or Experience Cloud site metadata, separately. ## Install Dependencies A generated app manages its dependencies with npm. Install them from inside the app's UIBundle directory, not the project root. ```shell cd force-app/main/default/uiBundles/MyReactApp npm install ``` The install brings in standard framework tooling (Vite for React, the Angular CLI for Angular), so there's no custom toolchain to learn. ## Next Steps After you generate and install your app: - Run it locally and see live changes: [Preview and Run Your App Locally](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-preview.md). - Understand the generated files and metadata: [Project Structure and Metadata](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-project-structure.md). - Follow the end-to-end path from scaffold to deploy: [Quick Start: Build and Run Your First App](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-quick-start.md). ## See Also - _Salesforce CLI Command Reference_: [template generate ui-bundle](https://developer.salesforce.com/docs/platform/salesforce-cli-reference/guide/cli_reference_template_generate_ui-bundle.html) - _Metadata API Developer Guide_: [UIBundle](https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_uibundle.htm) - [Multi-Framework recipes repo](https://github.com/trailheadapps/multiframework-recipes) # Get App Identity Use `getCurrentApp()` to discover the identity of the app that your code runs in. The SDK resolves the running app's namespace, name, and an addressable qualified name, so you can scope requests and target features to the current app. For example, use the app identity in these cases: - Scope a call to your app, such as when you run a CMS search - Pass the qualified name of your app as an addressable app name Import `getCurrentApp` from the `@salesforce/platform-sdk` package. ```typescript import { getCurrentApp } from "@salesforce/platform-sdk"; const app = await getCurrentApp(); const identity = app.identity; ``` ## getCurrentApp(options?) Resolves the identity of the running app. ```typescript async function getCurrentApp(options?: CurrentAppOptions): Promise; ``` ### Parameters | Parameter | Type | Description | | :--------- | :------------------ | :---------------------------------------------------------------------- | | `options?` | `CurrentAppOptions` | Optional configuration. Extends `SDKOptions` with a `surface` override. | ### Returns ```typescript Promise ``` `getCurrentApp()` resolves to a `CurrentApp` object. If you use `getCurrentApp()` outside of a web app or micro-frontend, such as in local development, `CurrentApp` is returned without the `identity` object. ## CurrentApp Interface Describes the running app. The `identity` object is present only when the runtime provides app identity. `identity` can be undefined, such as when it's called in local development, or if `appName` is missing. ```typescript interface CurrentApp { readonly identity?: AppIdentity; } ``` ## AppIdentity Interface Describes the identity of the running app. ```typescript interface AppIdentity { namespace: string; appName: string; qualifiedName: string; bundleId?: string; } ``` The `AppIdentity` interface has these properties. | Property | Type | Description | | :-------------- | :------- | :----------------------------------------------------------------------------------------------------- | | `namespace` | `string` | The app's namespace, which defaults to `c` when the org doesn't use a registered namespace. | | `appName` | `string` | The developer name of the UI bundle. | | `qualifiedName` | `string` | The addressable app name in the format `namespace__appName`, for example, `MyNs__MyBundle`. | | `bundleId?` | `string` | The record ID of the UI bundle, provided by the runtime. Defaults to `undefined` in local development. | ## Usage Check that `identity` is defined before you use it. If an app identity isn't available, the SDK resolves to a `CurrentApp` without an `identity` object instead of throwing, so your app keeps rendering. ```typescript import { getCurrentApp } from "@salesforce/platform-sdk"; const app = await getCurrentApp(); if (app.identity) { const targetIds = [app.identity.qualifiedName]; // ["MyNs__MyBundle"] } ``` To use `bundleId`, read it with a fallback and validate it before you use it. `bundleId` defaults to `undefined` in local development. ```typescript export async function getBundleId(): Promise { const app = await getCurrentApp(); return app.identity?.bundleId ?? ""; } ``` Since `getCurrentApp()` never rejects, you don't need a `.catch()` block. Handle the absence of identity in your code by checking that `identity` is defined, read `bundleId` with a fallback, and provide your default behavior when either is missing. ## Example: Use App Identity in a React Component Resolve the app identity inside an effect, and guard the asynchronous result against a component that unmounts before the promise settles. It's recommended to gate your logic on the value as `identity` can be undefined, such as in local development or when `appName` is missing. ```typescript import { useEffect, useState } from "react"; import { getCurrentApp } from "@salesforce/platform-sdk"; function useBundleId(): string | undefined { const [bundleId, setBundleId] = useState(); useEffect(() => { let cancelled = false; // getCurrentApp() resolves without rejecting, so you don't need a .catch(). // Handle the realistic case instead: the app resolves, but identity // (and bundleId) can be undefined on surfaces that don't provide it. getCurrentApp().then((app) => { if (cancelled) return; if (!app.identity?.bundleId) { // No bundleId on this surface. Fall back to your default behavior. setBundleId(undefined); return; } setBundleId(app.identity.bundleId); }); return () => { cancelled = true; }; }, []); return bundleId; } ``` Call `getCurrentApp()` where you need the identity. The SDK resolves the ambient app each time, so you don't cache the result in an app context or provider. ## See Also - [Work with Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-intro.md) - [GraphQL Queries in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-graphql.md) - [Access Record Data with Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-usage.md) # Get Custom Labels with the Labels Extension The `labels` extension is a Data SDK extension that reads custom labels from your org. Import it from `@salesforce/platform-sdk/data/extensions` and attach it to the SDK. The `labels` extension requires Salesforce API version 64.0 or later. See [Minimum API Version](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-extension-custom.md#set-a-minimum-api-version). ```typescript import { createDataSDK } from "@salesforce/platform-sdk/data"; import { labels } from "@salesforce/platform-sdk/data/extensions"; const sdk = await createDataSDK({ extensions: [labels({ namespace: "MyApp" })] }); // Fall back to the label name when a label doesn't resolve const greeting = (await sdk.ext.labels.get?.("greeting")) ?? "greeting"; // Override the configured options for a single call const french = await sdk.ext.labels.get?.("greeting", { locale: "fr" }); // Resolve several labels in one batched load const many = await sdk.ext.labels.getAll?.(["greeting", "farewell"]); ``` ## labels(config?) Creates the `labels` extension. Pass the optional `config` parameters to set default options for every call. Omit `config` to use the built-in defaults. `config` is of type `LabelsOptions`. ## LabelsOptions Sets where labels are read from and how they resolve. Supply these options when you configure the extension to set defaults, and again per call to override them for that call. Each field resolves independently as call value, then configured value, then built-in default. | Parameter | Type | Description | | :---------- | :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `namespace` | `string` | The custom-label namespace to read from. Omit it or pass an empty string to fall back per the precedence: in `labels()`, this falls back to the server's default namespace (`c`); per call, it falls back to the value configured in `labels()`, and then to the server default. | | `locale` | `string` | The target locale, such as `fr` or `en-US`. Omit it or pass an empty string to fall back per the precedence: in `labels()`, this falls back to the logged-in user's locale; per call, it falls back to the value configured in `labels()`, and then to the user's locale. | | `fallback` | `LabelFallback` | Specifies how the server resolves a label that has no translation for the requested locale. Defaults to `BASE_VALUE`. | ## LabelFallback Sets how the server resolves a label that has no translation for the requested locale. ```typescript type LabelFallback = "BASE_VALUE" | "USER_DEFAULT" | "NONE"; ``` - `BASE_VALUE`—Returns the org-default (base) value. This is the default. - `USER_DEFAULT`—Falls back to the logged-in user's language first, then the base value. - `NONE`—Doesn't fall back. A label with no translation for the locale is treated as unresolved, so it's absent from the result. ## LabelsApi The API returned under `sdk.ext.labels`. Both methods are optional and are absent on a surface whose Data SDK has no `graphql`. Use optional chaining (`sdk.ext.labels.get?.()`) to account for surfaces that don't support them. ### get() Resolves a single label. Returns its value, or `undefined` if the server doesn't resolve it. ```typescript get?(name: string, options?: LabelsOptions): Promise; ``` | Parameter | Type | Description | | :-------- | :-------------- | :------------------------------------------------------ | | `name` | `string` | The developer name of the label to resolve. | | `options` | `LabelsOptions` | Optional. Per-call overrides of the configured options. | Because an unresolved label returns `undefined`, provide your own fallback where you need one. ```typescript const greeting = (await sdk.ext.labels.get?.("greeting")) ?? "greeting"; ``` ### getAll() Resolves several labels in one batched load. Returns a map keyed only by the names that resolved. A name that doesn't resolve is absent from the map, which matches `get()` returning `undefined`. ```typescript getAll?(names: readonly string[], options?: LabelsOptions): Promise>; ``` | Parameter | Type | Description | | :-------- | :------------------ | :---------------------------------------------------------------------------------------------------------------- | | `names` | `readonly string[]` | The developer names of the labels to resolve. Names are deduped internally, so order and duplicates don't matter. | | `options` | `LabelsOptions` | Optional. Per-call overrides of the configured options. | Use `getAll()` for a bounded label set, such as an app's manifest of dozens to low hundreds of labels. Names are split into batches of 100, the server's per-call limit, and fetched concurrently. If you expect to resolve thousands of labels at once, we recommend that you batch, cache, or throttle the calls in your own code. ## See Also - [Data SDK Extensions](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-extension.md) - [Author a Custom Data SDK Extension](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-extension-custom.md) - [Work with Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-intro.md) # Get Started Go from an empty project to a running app on the Salesforce Platform. With Salesforce Multi-Framework, you build a web app with a frontend framework, such as React or Angular, run it locally against your org, and deploy it as a UIBundle. A UIBundle is a piece of Salesforce metadata that you manage with familiar Salesforce DX workflows. The fastest way to see how it fits together is the quick start, which scaffolds, runs, and deploys your first app in about 10 minutes. When you're ready to build your own app, work through the setup and development tasks in order. ## Build Your First App 1. **Set up your org.** Confirm prerequisites, enable the Salesforce app domain, and complete any optional configuration for Agentforce Conversation Client or Salesforce CMS. See [Set Up Your Org for Multi-Framework App Development](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-setup.md). 2. **Run the quick start.** Scaffold an app from a template, run it locally, and deploy it to your org. See [Quick Start: Build and Run Your First App](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-quick-start.md). ## The Multi-Framework App Lifecycle Whether you build in React or Angular, every Multi-Framework app follows the same path from an empty project to a published app that users can access. You often move back and forth between stages as your app evolves, but the overall journey is the same. Use this map to see where each topic in the guide fits. 1. **Set up your org.** Confirm prerequisites and enable the Salesforce app domain so that your org can host Multi-Framework apps. See [Set Up Your Org for Multi-Framework App Development](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-setup.md). 2. **Generate an app.** Scaffold a React or Angular app from a project template, either with an agentic coding tool or the Salesforce CLI. See [Generate an App from a Template](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-generate-app.md) and [Build with Agentic Coding Tools](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-agentic-tools.md). 3. **Learn your project structure.** Get to know the UIBundle project layout and the metadata that defines your app. See [Project Structure and Metadata](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-project-structure.md). 4. **Preview and run locally.** Run your app on your machine against your org for a fast feedback loop as you build. See [Preview and Run Your App Locally](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-preview.md). 5. **Add data, features, and style.** Connect to Salesforce data, integrate platform capabilities, and style your app. See [Access Salesforce Data and Platform Capabilities](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk/data-sdk-usage.md), [Style Your React Apps](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-styling.md), and [Integrate Agentforce Conversation Client](https://developer.salesforce.com/docs/platform/multiframework/guide/reactdev-acc.md). 6. **Test your app.** Run the unit and end-to-end tests that ship with your template before you deploy. See [Test Your App](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-testing.md). 7. **Deploy and publish.** Deploy the UIBundle to your org as metadata and publish a version so that users can access it. See [Deploy and Publish a UIBundle](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-deploy.md). 8. **Grant access and manage.** Manage the deployed app in Setup and give users access. See [Manage Your App and Grant User Access](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-manage.md). 9. **Distribute your app (optional).** Embed your app in Salesforce or package it for distribution to other orgs. See [Embed Your App in Salesforce with UI Embedding](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-ui-embedding.md) and [Package and Distribute Your App](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-packaging.md). ## See Also - [Salesforce Multi-Framework Overview](https://developer.salesforce.com/docs/platform/multiframework/guide/multiframework-overview.md) - [Multi-Framework recipes repo](https://github.com/trailheadapps/multiframework-recipes) # Get Started with UI Embedding Embed an existing external web app directly in Salesforce so your users work in one place instead of switching to a separate tab or window. With UI Embedding, Salesforce hosts your app inside a secure, sandboxed iframe by using the `lightning-ui-embedding` base component. This guide sets up the shared foundation, then walks you through the two configuration paths so you can choose the one that fits your project. **note:** UI Embedding was previously available as a Developer Preview. The Developer Preview used a different host element and setup flow. If you built against the preview, see [Migrate from the Developer Preview API](https://developer.salesforce.com/docs/platform/microfrontend/guide/migrate-from-dev-preview.md) before you move to the generally available version. ## Prerequisites - Your web app is hosted at an HTTPS URL that you control. - You have permission to create Lightning web components and edit Lightning pages or Experience Cloud sites in your Salesforce org. - Your Salesforce org is on a release where UI Embedding is generally available. You don't need to set a specific API version on the `lightning-ui-embedding` component. - UI Embedding is available in Enterprise, Performance, Unlimited, Developer, and Partner Developer editions, in orgs hosted on Hyperforce where English is the default language. ## Choose a Configuration Path You embed an app the same way in every case: you trust the app's domain, add the `lightning-ui-embedding` component to a page, and point it at your app. What differs is how your app talks back to Salesforce. Pick the path that matches how much you want to depend on a Salesforce package. | Path | Use when | How the app communicates | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | With a Salesforce package | You want the fastest setup and a supported, typed API. | Your app imports the platform SDK, which manages the communication channel for you. | | Without a Salesforce package | You can't or don't want to add a Salesforce package to your app's build, or you want full control of the communication layer. | Your app implements the `sf-embedding` protocol directly over the channel Salesforce provides. | Both paths use the same host component and the same underlying protocol, so you can start with one and move to the other later without re-architecting the embedding. ## Trust Your App's Domain This step is the same for both paths. Salesforce loads your app in an iframe, so you first tell Salesforce to trust your app's origin. From Setup, add your app's origin to your trusted URL settings for inline frames. **important:** Use the exact origin that serves your app, including the scheme and any subdomain. An origin that doesn't match the app's real origin prevents the app from loading. The app's origin must also differ from your Salesforce org's origin. A same-origin `src` is rejected with a `SAME_ORIGIN_SRC` error, so host the app on a different origin. ## Add the Embedding Component This step is also the same for both paths. Create a Lightning web component that renders `lightning-ui-embedding` and points it at your app's URL. 1. In your Salesforce DX project, create a Lightning web component. This component is a thin wrapper around the embedding component. 2. In the component's template, add `lightning-ui-embedding` and set the required `src` attribute to your app's URL. ```html ``` ```javascript // myEmbeddedApp.js import { LightningElement } from "lwc"; export default class MyEmbeddedApp extends LightningElement { appUrl = "https://app.example.com"; } ``` **note:** This code is a starting sketch. Replace `https://app.example.com` with your app's URL. To give the embedded frame an accessible name, set the `title` attribute; if you don't, the frame uses the default name `Embedded widget`. The `src` attribute is required, and Salesforce reads it when the component mounts. Because `src` binds the session to a specific app, don't change it after the component renders. This is enforced: changing `src` (or `sandbox`) after mount fires an `sf-embedding.component.error` with the code `SESSION_BINDING_MUTATED`. To load a different URL, unmount and remount the component. Then place your wrapper on a page like any other Lightning web component. 1. Open Lightning App Builder for a Lightning page, or open your Experience Cloud site in Experience Builder. 2. Drag your custom component onto the page. 3. Save and activate the page. Your embedded app now renders inside Salesforce. Next, choose a configuration path to connect your app to Salesforce. ## Path 1: Configure With a Salesforce Package On this path, your app depends on the Salesforce platform SDK, which manages the communication channel for you. You get a supported, typed API for the lifecycle, UI state, resizing, and dirty-state features without writing any protocol code. 1. Add the platform SDK to your app and import the UI Embedding entry point. ```javascript import "@salesforce/platform-sdk/ui-embedding"; ``` 2. Handle the component's ready and error lifecycle events on your wrapper. The embedding component reports its lifecycle through DOM events, so listen for them so your wrapper can respond when the app is ready or when embedding fails. - `sf-embedding.component.ready` fires when the embedded app finishes loading and is ready to interact with Salesforce. - `sf-embedding.component.error` fires when embedding fails. The event detail includes the phase (`configuration` or `bootstrap`), an error code, a message, and whether the error is retryable. For the full list of codes, see [UI Embedding Error Codes](https://developer.salesforce.com/docs/platform/microfrontend/references/microfrontend-error-codes/error-codes.html). These event names contain dots, so you can't bind them declaratively with `on*` attributes in the template. Add `lwc:ref` to the embedding element and attach the listeners imperatively in `renderedCallback`. ```html ``` ```javascript // myEmbeddedApp.js import { LightningElement } from "lwc"; export default class MyEmbeddedApp extends LightningElement { appUrl = "https://app.example.com"; hasWiredEvents = false; renderedCallback() { if (this.hasWiredEvents) { return; } this.hasWiredEvents = true; const el = this.refs.embedding; el.addEventListener("sf-embedding.component.ready", this.handleReady); el.addEventListener("sf-embedding.component.error", this.handleError); } handleReady = () => { // The embedded app is ready. }; handleError = (event) => { const { code, message } = event.detail; // Handle the failure. }; } ``` 3. In your embedded app, create the view SDK to read UI state and use the embedding features. `createViewSDK` is asynchronous, and off an embedding surface it resolves to an empty object, so guard the calls you make on it. ```javascript import { createViewSDK } from "@salesforce/platform-sdk/view"; const viewSdk = await createViewSDK(); const uiState = viewSdk.getUiState?.(); ``` With the view SDK in place, continue with the common tasks: - [Exchange Data Between the Page and an Embedded App](https://developer.salesforce.com/docs/platform/microfrontend/guide/exchange-data.md) - [Style an Embedded App to Match Salesforce](https://developer.salesforce.com/docs/platform/microfrontend/guide/style-embedded-app.md) - [Resize an Embedded App to Fit Its Content](https://developer.salesforce.com/docs/platform/microfrontend/guide/resize-embedded-app.md) - [Warn Users About Unsaved Changes in an Embedded App](https://developer.salesforce.com/docs/platform/microfrontend/guide/track-unsaved-changes.md) ## Path 2: Configure Without a Salesforce Package On this path, your app doesn't take a dependency on any Salesforce package. Instead, your app implements the `sf-embedding` protocol directly: it completes the handshake, then sends and receives the protocol messages itself. Choose this path when you can't add a Salesforce package to your app's build, or when you want full control of the communication layer. The `lightning-ui-embedding` component and the domain-trust and page setup are identical to the with-package path. The difference is entirely inside your app, where you build the communication bridge by hand. For the full walkthrough, including the handshake, the protocol message catalog, error handling, and security requirements, see [Build the Embedding Bridge Without a Salesforce Package](https://developer.salesforce.com/docs/platform/microfrontend/guide/package-free-bridge.md). ## Scaffold a Starter Component from the CLI Instead of writing the wrapper by hand, generate one with the Salesforce CLI. The `sf template generate ui-embedding` command scaffolds an embedding-ready Lightning web component bundle. ```bash sf template generate ui-embedding --name myEmbeddedApp --src https://app.example.com ``` The command creates a Lightning web component bundle configured for embedding. The bundle includes the HTML, JavaScript, metadata, and CSS files. Then edit the generated component and add it to a page as described earlier. **note:** This command requires a recent version of the Salesforce CLI. If the command isn't available, update the CLI, then run `sf template generate ui-embedding --help` to see the current flags and syntax. ## See Also - [Exchange Data Between the Page and an Embedded App](https://developer.salesforce.com/docs/platform/microfrontend/guide/exchange-data.md) - [Build the Embedding Bridge Without a Salesforce Package](https://developer.salesforce.com/docs/platform/microfrontend/guide/package-free-bridge.md) - [UI Embedding Error Codes](https://developer.salesforce.com/docs/platform/microfrontend/references/microfrontend-error-codes/error-codes.html) # GraphQL Mutate Parameters The GraphQL `mutate()` method in the Data SDK includes several parameters. ```typescript import { createDataSDK, gql } from "@salesforce/platform-sdk/data"; const dataSdk = await createDataSDK(); const result = await dataSdk.graphql?.mutate({ mutation: MUTATION_STRING, variables: { ... }, operationName?: "...", headers?: { "My-Header": "value" } }); ``` ## `graphql.mutate>(options: MutateOptions)` Runs a GraphQL mutation without caching. ### Parameters | Parameter | Type | Description | | :--------------- | :----------------------------------------- | :------------------------------------------------------------------------------------------------------ | | `mutation` | `string` | The GraphQL mutation operation string. | | `variables?` | `V`, defaults to `Record` | Optional key-value map of GraphQL variables referenced by the mutation. | | `operationName?` | `string` | Optional operation name for multi-operation documents. | | `headers?` | `HeadersInit` | Optional [per-request HTTP headers](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-request-headers.md) merged over the SDK's default headers. | ## UI API Mutation Structure All GraphQL mutations follow this UI API structure. ```typescript mutation ($input: Input!) { uiapi { (input: $input) { # Create/Update: return Record with selected fields Record { Id { value } } # Delete: return only Id Id } } } ``` For more information, see [Mutations Schema](https://developer.salesforce.com/docs/platform/graphql/guide/mutations-schema.html) in the _GraphQL API Developer Guide_. ## Inline Mutations with gql Tag For simple mutations without external `.graphql` files, use the `gql` template tag. ```typescript import { createDataSDK, gql } from "@salesforce/platform-sdk/data"; const DELETE_ACCOUNT = gql` mutation DeleteAccount($id: ID!) { uiapi { AccountDelete(input: { Id: $id }) { Id } } } `; export async function deleteAccount(id: string) { const dataSdk = await createDataSDK(); const result = await dataSdk.graphql?.mutate({ mutation: DELETE_ACCOUNT, variables: { id }, }); if (result?.errors?.length) { throw new Error("Failed to delete account"); } return result?.data?.uiapi?.AccountDelete?.Id; } ``` ### Inline Mutations Use inline mutations when: - Simple mutations without complex input structures - Mutations used in only one place - Rapid prototyping or testing ### External `.graphql` Files Use external `.graphql` files when: - Complex mutations with many fields - Mutations shared across multiple files - When you want full type generation from codegen For more information on generating types and implementing a mutation operation, see [GraphQL Mutations in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-graphql-mutation.md). ## Returns ```typescript Promise>; ``` The return type of `dataSdk.graphql?.mutate()`, which resolves to the typed GraphQL response payload. ```typescript interface MutationResult { data: T | undefined; errors?: GraphQLError[]; } ``` **note:** Mutations don't support `subscribe()` or `refresh()` like [GraphQL queries](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-query.md). To update stale data after a mutation, use a query result reference to call `result.refresh()`. Consider these mutation response behavior: - A successful mutation returns data with no errors. - A partially successful mutation has both data and errors. - Data SDK preserves the partial data payload, so callers can still use returned fields even when errors exist. GraphQL mutation operations return these response structures. ### Record Create A create operation returns the record ID and any queried fields. ```typescript { data: { uiapi: { AccountCreate?: { Record?: { Id: string; Name?: { value?: string | null }; Industry?: { value?: string | null }; // other returned fields... } | null; } | null; }; }; errors?: Array<{ message: string; locations?: Array<{ line: number; column: number }>; path?: string[]; }>; } ``` ### Record Update An update operation returns the record ID and the updated fields. ```typescript { data: { uiapi: { AccountUpdate?: { Record?: { Id: string; Name?: { value?: string | null }; // returned updated fields... } | null; } | null; }; }; errors?: Array<{ message: string; locations?: Array<{ line: number; column: number }>; path?: string[]; }>; } ``` ### Record Delete A delete operation returns the deleted record ID only. ```typescript { data: { uiapi: { AccountDelete?: { Id: string; } | null; }; }; errors?: [ { message: string; locations?: [{ line: number; column: number }]; path?: string[]; } ]; } ``` ## Example: Successful Create Record Response A successful query response includes record data in the `RecordCreate` object. ```json { "data": { "uiapi": { "AccountCreate": { "Record": { "Id": "001xx000003DHP0AAO", "Name": { "value": "Acme Corp" }, "Industry": { "value": "Technology" } } } } } } ``` ## Example: Partially Successful Create Record Response A partially successful query response includes errors and some data. ```json { "data": { "uiapi": { "AccountCreate": { "Record": { "Id": "001xx000003DHP0AAO", "Name": { "value": "Acme Corp" } } } } }, "errors": [ { "message": "Insufficient access on field AnnualRevenue", "locations": [{ "line": 8, "column": 5 }], "path": ["uiapi", "AccountCreate", "Record", "AnnualRevenue"] } ] } ``` ## See Also - [Error Handling in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-graphql-error.md) - [Cache Control in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-cache-control.md) - _GraphQL API Developer Guide_: [Send Mutation Requests](https://developer.salesforce.com/docs/platform/graphql/guide/mutations-use.html) # GraphQL Mutations in Data SDK GraphQL mutations are operations that create, update, or delete Salesforce records. Use the `graphql.mutate()` method for all mutation operations. Unlike queries, mutations: - Are never cached and don't write to the cache - Return `MutationResult` with only `data` and `errors` fields - Don't provide subscription support - Don't write to the cache, so they won't affect subsequent query results ```typescript const result = await dataSdk.graphql?.mutate({ mutation: MUTATION_STRING, variables: { ... }, operationName?: "..." }); // Check for errors if (result?.errors?.length) { throw new Error(result.errors.map((e) => e.message).join("; ")); } // Use the returned data const createdRecord = result?.data; ``` ## Create Record Create a new record using the `Create` mutation. ### Step 1: Define the Mutation A mutation operation follows the `RecordCreate` name pattern, for example, `AccountCreate`. See [Mutations Schema](https://developer.salesforce.com/docs/platform/graphql/guide/mutations-schema.html) in the _GraphQL API Developer Guide_. ```graphql # uiBundles/yourAppName/src/api/account/query/createAccount.graphql mutation CreateAccount($input: AccountCreateInput!) { uiapi { AccountCreate(input: $input) { Record { Id Name { value } Industry { value } Phone { value } Website { value } } } } } ``` ### Step 2: Generate Types Run `npm run graphql:codegen` to generate TypeScript types. ```typescript // Generated in src/api/graphql-operations-types.ts export type CreateAccountMutation = { uiapi: { AccountCreate: { Record: { Id: string; Name: { value: string } | null; Industry: { value: string } | null; Phone: { value: string } | null; Website: { value: string } | null; }; }; }; }; export type CreateAccountMutationVariables = { input: AccountCreateInput; }; export type AccountCreateInput = { Account: { Name?: string; Industry?: string; Phone?: string; Website?: string; // ... other fields }; }; ``` ### Step 3: Implement the Mutation Function Implement the client-side mutation function that invokes `dataSdk.graphql?.mutate()` with the generated types. ```typescript // uiBundles/yourAppName/src/api/account/accountCreate.ts import { createDataSDK } from "@salesforce/platform-sdk/data"; import CREATE_ACCOUNT from "./mutation/createAccount.graphql?raw"; import type { CreateAccountMutation, CreateAccountMutationVariables, } from "../graphql-operations-types"; export interface CreateAccountInput { name: string; industry?: string; phone?: string; website?: string; } export async function createAccount(input: CreateAccountInput) { const dataSdk = await createDataSDK(); const result = await dataSdk.graphql?.mutate< CreateAccountMutation, CreateAccountMutationVariables >({ mutation: CREATE_ACCOUNT, variables: { input: { Account: { Name: input.name, Industry: input.industry, Phone: input.phone, Website: input.website, }, }, }, }); if (result?.errors?.length) { const errorMessages = result.errors.map((e) => e.message).join("; "); throw new Error(`Failed to create account: ${errorMessages}`); } return result?.data?.uiapi?.AccountCreate?.Record; } ``` ### Step 4: Use in Your Component This component example shows how to call `createAccount`, handle loading and error states, and react to the created result. ```typescript import { useState } from "react"; import { createAccount } from "./api/accounts"; function CreateAccountForm() { const [name, setName] = useState(""); const [industry, setIndustry] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setLoading(true); setError(null); try { const newAccount = await createAccount({ name, industry }); console.log("Created account:", newAccount); // Clear form setName(""); setIndustry(""); // Optionally navigate or refresh data } catch (err) { setError((err as Error).message); } finally { setLoading(false); } } return (
setName(e.target.value)} placeholder="Account Name" required /> setIndustry(e.target.value)} placeholder="Industry" /> {error &&
{error}
}
); } ``` ## Update Record Update an existing record using the `Update` mutation. ### Step 1: Define the Mutation ```graphql # uiBundles/yourAppName/src/api/account/query/updateAccount.graphql mutation UpdateAccount($input: AccountUpdateInput!) { uiapi { AccountUpdate(input: $input) { Record { Id Name { value } Industry { value } AnnualRevenue { value } Phone { value } } } } } ``` ### Step 2: Implement the Mutation Function Implement the update function: call `dataSdk.graphql?.mutate()` with the update payload, then check and handle any returned errors. ```typescript import { createDataSDK } from "@salesforce/platform-sdk/data"; import UPDATE_ACCOUNT from "./mutation/updateAccount.graphql?raw"; import type { UpdateAccountMutation, UpdateAccountMutationVariables, } from "../graphql-operations-types"; export interface UpdateAccountInput { id: string; name?: string; industry?: string; annualRevenue?: number; phone?: string; } export async function updateAccount(input: UpdateAccountInput) { const dataSdk = await createDataSDK(); const result = await dataSdk.graphql?.mutate< UpdateAccountMutation, UpdateAccountMutationVariables >({ mutation: UPDATE_ACCOUNT, variables: { input: { Id: input.id, Account: { Name: input.name, Industry: input.industry, AnnualRevenue: input.annualRevenue, Phone: input.phone, }, }, }, }); if (result?.errors?.length) { const errorMessages = result.errors.map((e) => e.message).join("; "); throw new Error(`Failed to update account: ${errorMessages}`); } return result?.data?.uiapi?.AccountUpdate?.Record; } ``` ### Step 3: Use in Your Component This form component demonstrates using `updateAccount` and calling `onSuccess` after a successful update, while displaying errors when present. ```typescript // uiBundles/yourAppName/src/api/account/accountUpdate.ts import { useState, useEffect } from "react"; import { updateAccount } from "./api/accounts"; interface EditAccountFormProps { accountId: string; initialName: string; initialIndustry?: string; onSuccess?: () => void; } function EditAccountForm({ accountId, initialName, initialIndustry, onSuccess, }: EditAccountFormProps) { const [name, setName] = useState(initialName); const [industry, setIndustry] = useState(initialIndustry || ""); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setLoading(true); setError(null); try { await updateAccount({ id: accountId, name, industry, }); onSuccess?.(); } catch (err) { setError((err as Error).message); } finally { setLoading(false); } } return (
setName(e.target.value)} placeholder="Account Name" required /> setIndustry(e.target.value)} placeholder="Industry" /> {error &&
{error}
}
); } ``` ## Delete Record Delete a record using the `Delete` mutation. ### Step 1: Define the Mutation Define the delete mutation in a `.graphql` file that accepts the delete input and returns the deleted record Id. ```graphql # uiBundles/yourAppName/src/api/account/query/deleteAccount.graphql mutation DeleteAccount($input: AccountDeleteInput!) { uiapi { AccountDelete(input: $input) { Id } } } ``` **note:** Delete mutations only return the ID of the deleted record, not a `Record` object. ### Step 2: Implement the Mutation Function Implement the delete function that calls `dataSdk.graphql?.mutate()`, validates the result, and returns the deleted ID to callers. ```typescript import { createDataSDK } from "@salesforce/platform-sdk/data"; import DELETE_ACCOUNT from "./mutation/deleteAccount.graphql?raw"; import type { DeleteAccountMutation, DeleteAccountMutationVariables, } from "../graphql-operations-types"; export async function deleteAccount(id: string): Promise { const dataSdk = await createDataSDK(); const result = await dataSdk.graphql?.mutate< DeleteAccountMutation, DeleteAccountMutationVariables >({ mutation: DELETE_ACCOUNT, variables: { input: { Id: id, }, }, }); if (result?.errors?.length) { const errorMessages = result.errors.map((e) => e.message).join("; "); throw new Error(`Failed to delete account: ${errorMessages}`); } return result?.data?.uiapi?.AccountDelete?.Id || ""; } ``` ### Step 3: Use in Your Component This example shows a button component that calls `deleteAccount`, displays progress, and handles any error returned by the mutation. ```typescript // uiBundles/yourAppName/src/api/account/accountDelete.ts import { useState } from "react"; import { deleteAccount } from "./api/accounts"; interface DeleteAccountButtonProps { accountId: string; accountName: string; onSuccess?: () => void; } function DeleteAccountButton({ accountId, accountName, onSuccess }: DeleteAccountButtonProps) { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); async function handleDelete() { const confirmed = window.confirm(`Are you sure you want to delete "${accountName}"?`); if (!confirmed) return; setLoading(true); setError(null); try { await deleteAccount(accountId); onSuccess?.(); } catch (err) { setError((err as Error).message); } finally { setLoading(false); } } return (
{error &&
{error}
}
); } ``` ## Refresh Queries After Mutations Since mutations don't update the cache, you need to manually refresh related queries to reflect the changes in the UI. ### Pattern 1: Refresh with QueryResult If you have access to the `QueryResult` object from a previous query, call `refresh()`: ```typescript import { useEffect, useState } from "react"; import { createDataSDK } from "@salesforce/platform-sdk/data"; import { createAccount } from "./api/accounts"; import type { QueryResult } from "@salesforce/platform-sdk/data"; function AccountManager() { const [accountsResult, setAccountsResult] = useState>(); const [accounts, setAccounts] = useState([]); useEffect(() => { async function loadAccounts() { const dataSdk = await createDataSDK(); const result = await dataSdk.graphql?.query({ query: GET_ACCOUNTS, }); setAccountsResult(result); setAccounts(result?.data?.uiapi?.query?.Account?.edges?.map((e) => e.node) || []); // Subscribe to updates result?.subscribe((snapshot) => { setAccounts(snapshot.data?.uiapi?.query?.Account?.edges?.map((e) => e.node) || []); }); } loadAccounts(); }, []); async function handleCreateAccount(name: string) { try { // Perform the mutation await createAccount({ name }); // Refresh the query to show the new record if (accountsResult) { await accountsResult.refresh(); } } catch (error) { console.error("Failed to create account:", error); } } return (
    {accounts.map((account) => (
  • {account.Name?.value}
  • ))}
); } ``` ### Pattern 2: Re-query After Mutation If you don't have access to the `QueryResult`, re-execute the query. ```typescript async function handleCreateAccount(name: string) { try { // Perform the mutation await createAccount({ name }); // Re-query await loadAccounts(); } catch (error) { console.error("Failed to create account:", error); } } ``` ### Pattern 3: Optimistic Updates You can choose to update the UI before the mutation completes. ```typescript async function handleDeleteAccount(accountId: string) { // Optimistically remove from UI setAccounts((prev) => prev.filter((a) => a.Id !== accountId)); try { await deleteAccount(accountId); // Success - optimistic update was correct } catch (error) { console.error("Failed to delete account:", error); // Revert optimistic update if (accountsResult) { await accountsResult.refresh(); } } } ``` ## Mutation Best Practices Here are several best practices for working with GraphQL mutation. ### Select Id in Return Fields Include `Id` in your mutation return fields. ```graphql mutation CreateAccount($input: AccountCreateInput!) { uiapi { AccountCreate(input: $input) { Record { Id # Always include this Name { value } } } } } ``` ### Use Type-Safe Input Interfaces Define interfaces for mutation inputs for type safety. ```typescript export interface CreateContactInput { firstName: string; lastName: string; accountId?: string; email?: string; phone?: string; } export async function createContact(input: CreateContactInput) { // Type-safe mutation implementation } ``` ### Handle Loading States Show loading states during mutations to notify users on what to expect. ```typescript const [isCreating, setIsCreating] = useState(false); async function handleCreate() { setIsCreating(true); try { await createAccount({ name: "New Account" }); } finally { setIsCreating(false); } } ``` ### Provide User Feedback Show success and error messages to communicate expectations with users. ```typescript const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null); async function handleCreate() { try { await createAccount({ name: "New Account" }); setMessage({ type: "success", text: "Account created successfully!" }); } catch (error) { setMessage({ type: "error", text: (error as Error).message }); } } ``` ### Clean Up Form State Reset form fields after successful mutations. ```typescript async function handleSubmit(e: React.FormEvent) { e.preventDefault(); try { await createAccount({ name, industry }); // Clear form on success setName(""); setIndustry(""); } catch (error) { // Handle error } } ``` ### Batch Related Mutations If you must perform multiple mutations, consider batching them. ```typescript async function createAccountWithContacts( accountData: CreateAccountInput, contacts: CreateContactInput[], ) { // Create account first const account = await createAccount(accountData); if (!account?.Id) { throw new Error("Failed to create account"); } // Create contacts with the new account ID const contactPromises = contacts.map((contact) => createContact({ ...contact, accountId: account.Id }), ); await Promise.all(contactPromises); return account; } ``` ## See Also - [GraphQL Queries in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-graphql.md) - [Error Handling in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-graphql-error.md) - [Cache Control in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-cache-control.md) # GraphQL Queries in Data SDK Use a GraphQL query to retrieve Salesforce data, such as from an Account or Contact record. To access Salesforce data via GraphQL: 1. Define TypeScript interfaces for the response shape. 2. Write the GraphQL query using `uiapi` syntax. 3. Use `createDataSDK()` and `dataSdk.graphql?.()` with proper typing. 4. Extract and return the data from the response. ```typescript import { createDataSDK, gql } from "@salesforce/platform-sdk/data"; const dataSdk = await createDataSDK(); // For queries: use dataSdk.graphql?.query() or dataSdk.fetch?() for API calls const result = await dataSdk.graphql?.query({ query: QUERY_STRING, variables: { ... }, operationName?: "...", cacheControl?: "no-cache" | "only-if-cached" | { type: "max-age", maxAge: 60 } }); const QUERY_STRING = gql` query MyQuery { uiapi { ... } } `; ``` For more information about GraphQL usage, see the [GraphQL API Developer Guide](https://developer.salesforce.com/docs/platform/graphql/guide/). Use one of these patterns for defining GraphQL queries. - [Inline `gql` Tag](#inline-gql-tag) - [External `.graphql` File](#external-graphql-file) ## Inline `gql` Tag This pattern is useful for simple queries without variables or fragments. - Use the gql template tag (never plain template literals) - Ensure the query operation name matches what codegen expects - Import generated types from graphql-operations-types.ts ```javascript import { createDataSDK, gql } from "@salesforce/platform-sdk/data"; import type { CurrentUserQuery } from "../graphql-operations-types"; const CURRENT_USER_QUERY = gql` query CurrentUser { uiapi { currentUser { Id Name { value } } } } `; interface User { id: string; name: string; } export async function getCurrentUser(): Promise { const dataSdk = await createDataSDK(); const result = (await dataSdk.graphql?.query) < CurrentUserQuery > { query: CURRENT_USER_QUERY, }; if (result?.errors?.length) { throw new Error(result.errors.map((e) => e.message).join("; ")); } const userData = result?.data?.uiapi.currentUser; if (!userData) { throw new Error("No user data found"); } return { id: userData.Id, name: userData.Name?.value || "User", }; } ``` ## External .graphql File This pattern is useful for complex queries with variables, fragments, or queries shared across files. - Import `.graphql` files with the `?raw` suffix - Import generated types from `graphql-operations-types.ts` - Provide response and variables type generics to `dataSdk.graphql?.query()` - Use optional chaining for nested response data To implement a query using a `.graphql` external file: 1. Create a `.graphql` file under `src/api/utils/query/` ```javascript # src/api/utils/query/myQuery.graphql query GetMyData($myVariable: String) { uiapi { query { MyObject(first: 10, where: { Field: { eq: $myVariable } }) { edges { node { Id Name { value } } } } } } } ``` 2. Generate the types using `npm run graphql:codegen`. The command generates the types in `src/api/graphql-operations-types.ts`. For example: 1. Response type: `GetMyDataQuery` 2. Variables type: `GetMyDataQueryVariables` 3. Implement the data access function. ```javascript import { createDataSDK, type NodeOfConnection } from "@salesforce/platform-sdk/data"; import MY_QUERY from "./query/myQuery.graphql?raw"; import type { GetMyDataQuery, GetMyDataQueryVariables } from "../graphql-operations-types"; type MyNode = NodeOfConnection; export async function getMyData(variables: GetMyDataQueryVariables): Promise { const dataSdk = await createDataSDK(); const result = await dataSdk.graphql?.query({ query: MY_QUERY, variables, }); if (result?.errors?.length) { const errorMessages = result.errors.map((e) => e.message).join("; "); throw new Error(`GraphQL Error: ${errorMessages}`); } return result?.data?.uiapi?.query?.MyObject?.edges?.map((edge) => edge?.node) || []; } ``` ## Export the GraphQL Return Type To provide type safety for GraphQL operations, define your return type. These TypeScript interfaces define the shape of data flowing in (query variables) and out (query results) of your GraphQL operations. ```typescript export interface Account { id: string; name: string; industry: string | null; type: string | null; annualRevenue: number | null; numberOfEmployees: number | null; billingCity: string | null; billingState: string | null; billingCountry?: string | null; phone: string | null; website: string | null; description?: string | null; } export interface AccountsResult { accounts: Account[]; pageInfo: { hasNextPage: boolean; endCursor: string | null; }; } export interface GetAccountsQueryVariables { first?: number | null; after?: string | null; } ``` Export the interfaces so they can be imported and reused across components. You can also use graphql-codegen to generate the interfaces for the interfaces to stay in sync with your schema. Use the return types in your methods and variables. ```typescript export async function getAccounts(first = 20, after?: string): Promise { const variables: GetAccountsQueryVariables = { first }; if (after) variables.after = after; const dataSdk = await createDataSDK(); const result = await dataSdk.graphql?.query({ query: GET_ACCOUNTS, variables, }); if (result?.errors?.length) { throw new Error(result.errors.map((e) => e.message).join("; ")); } ``` ## GraphQL Fragments GraphQL fragments are reusable units of query logic that are used to define a set of fields. This example uses GraphQL fragments and directives to keep one query reusable while conditionally including fields. - `FinancialFields` and `ContactFields` are reusable field groups for `Account`. - In the main query, each fragment is attached with `@include(if: $...):` - `...FinancialFields @include(if: $includeFinancials)` - `...ContactFields @include(if: $includeContacts)` - At runtime, variables (`includeFinancials, includeContacts`) decide whether those field blocks are fetched. ```javascript query GetAccountDetails($id: ID!, $includeFinancials: Boolean!, $includeContacts: Boolean!) { uiapi { query { Account(where: { Id: { eq: $id } }) { edges { node { Id Name { value } ...FinancialFields @include(if: $includeFinancials) ...ContactFields @include(if: $includeContacts) } } } } } } fragment FinancialFields on Account { AnnualRevenue { value } NumberOfEmployees { value } } fragment ContactFields on Account { Phone { value } Website { value } } ``` `dataSdk.graphql?.query({ query, variables })` executes the query with: - explicit response type (`GetAccountDetailsQuery`) - explicit variables type (`GetAccountDetailsQueryVariables`) - runtime variables object (`id, includeFinancials, includeContacts`) ```typescript import { createDataSDK } from "@salesforce/platform-sdk/data"; import QUERY from "./query/getAccountDetails.graphql?raw"; import type { GetAccountDetailsQuery, GetAccountDetailsQueryVariables, } from "../graphql-operations-types"; const dataSdk = await createDataSDK(); const result = await dataSdk.graphql?.query< GetAccountDetailsQuery, GetAccountDetailsQueryVariables >({ query: QUERY, variables: { id: accountId, includeFinancials: userWantsFinancials, includeContacts: userWantsContacts, }, }); ``` ## GraphQL Usage Considerations When using GraphQL via the Data SDK, consider these usage patterns. Direct API calls aren't supported. Always use the Data SDK to make your API calls. ```typescript // Don't do this: Direct axios/fetch for Salesforce APIs const result = await axios.post("/graphql", { query }); const result = await fetch("/services/data/v{version}/..."); // Do this: Use the DataSDK const dataSdk = await createDataSDK(); const result = await dataSdk.graphql?.query({ query, variables }); const result = await dataSdk.fetch?.("/services/data/v{version}/..."); ``` Provide the response type instead of making untyped GraphQL calls. ```typescript // Don't do this: Untyped GraphQL calls await dataSdk.graphql?.query({ query }); // Do this: Provide response type generic await dataSdk.graphql?.query({ query }); ``` Use the gql template tag or an external .graphql file instead of plain string queries. ```typescript // Don't do this: Plain string queries const query = `query { ... }`; // Do this: Use gql tag or .graphql file const query = gql`query { ... }`; // or do this: Import the query from an external .graphql file import QUERY from "./query/myQuery.graphql?raw"; ``` Use optional chaining instead of non-null assertion on a GraphQL call. ```typescript // Don't do this: Non-null assertion on graphql await dataSdk.graphql!.query({ query }); // Do this: Optional chaining await dataSdk.graphql?.query({ query }); ``` ## See Also - [GraphQL Mutations in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-graphql-mutation.md) - [Error Handling in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-graphql-error.md) - [Cache Control in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-cache-control.md) # GraphQL Query Parameters The GraphQL `query()` method in the Data SDK includes several parameters. ```typescript import { createDataSDK, gql } from "@salesforce/platform-sdk/data"; const dataSdk = await createDataSDK(); const result = await dataSdk.graphql?.query({ query: QUERY_STRING, variables: { ... }, operationName?: "...", cacheControl?: "no-cache" | "only-if-cached" | { type: "max-age", maxAge: 60 }, headers?: { "My-Header": "value" } }); const QUERY_STRING = gql` query MyQuery { uiapi { ... } } `; ``` ## `graphql.query>(options: QueryOptions)` Runs a GraphQL query with reactive capabilities. ### Parameters | Parameter | Type | Description | | :--------------- | :----------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `query` | `string` | The GraphQL query operation string, which accepts an [inline `gql` template tag or an external `.graphql` file](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-graphql.md). | | `variables?` | `V`, defaults to `Record` | Optional key-value map of GraphQL variables referenced by the query. | | `operationName?` | `string` | Optional operation name for multi-operation documents. | | `cacheControl?` | `CacheControl` | Optional [cache control settings](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-cache-control.md) for the GraphQL request. | | `headers?` | `HeadersInit` | Optional [per-request HTTP headers](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-request-headers.md) merged over the SDK's default headers. Because they can scope the response (for example, `Authorization`), `headers` is part of the cache key. See [Cache Control](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-cache-control.md). | ## UI API Query Structure All GraphQL queries follow this UI API structure. ```javascript query { uiapi { query { ( first: Int # pagination limit after: String # pagination cursor where: _Filter orderBy: _OrderBy ) { edges { node { Id { value } } } } } } } ``` For more information, see [Query Objects](https://developer.salesforce.com/docs/platform/graphql/guide/query-record-objects.html) in the _GraphQL API Developer Guide_. ## Returns ### **`Promise>`** The return type of `dataSdk.graphql?.query()`, which resolves to the typed GraphQL response payload. ```typescript interface QueryResult { data: T | undefined; errors?: GraphQLError[]; subscribe(cb: QuerySubscriber): Unsubscribe; refresh(): Promise; } ``` GraphQL record queries return this response structure. ```typescript interface UIAPIQueryResponse { uiapi: { query: { [ObjectName: string]: { edges?: Array<{ node?: { Id: string; // Every field is a { value, displayValue } envelope, not a bare scalar. // - value: raw value — use for logic and for writing back to the server // - displayValue: locale-formatted string — use for rendering in the UI [FieldName: string]?: { value?: FieldType | null; displayValue?: string | null } | null; } | null; } | null> | null; } | null; }; }; } ``` ## Subscribe to Query Updates Query results from `graphql.query()` are reactive and support real-time updates through subscriptions, which you can use to update your UI automatically when data changes. The `subscribe()` method listens for updates to a query result. Subscribers are notified when the data changes due to cache updates or explicit refreshes. ```typescript const result = await dataSdk.graphql?.query({ query: GET_ACCOUNT, variables: { id: accountId }, }); // Subscribe to updates const unsubscribe = result.subscribe((snapshot) => { console.log("Updated data:", snapshot.data); if (snapshot.errors) { console.warn("Errors:", snapshot.errors); } // Update your UI here }); // Clean up when done unsubscribe(); ``` ## Refresh Query Data Use the `refresh()` method to manually re-fetch data, bypassing the cache. ```typescript const result = await dataSdk.graphql?.query({ query: GET_ACCOUNT, variables: { id: accountId }, }); // Subscribe to updates result.subscribe((snapshot) => { // UI update logic setAccount(snapshot.data); }); // Later, refresh the data (e.g., on button click) async function handleRefresh() { await result.refresh(); // Notify subscribers } ``` ## Example: Query Records from a React App Here's an example on how to query records. The example uses the [`useState`](https://react.dev/reference/react/useState) and [`useEffect`](https://react.dev/reference/react/useEffect) React hooks to manage the component state and respond to data changes. ``` /** * List Rendering with Salesforce Records * * Fetches Accounts via UIAPI GraphQL and renders them using .map(). * Each item needs a stable key prop; here we use the record Id. * * LWC equivalent: list rendering uses for:each / lwc:for directives on * template elements with a key attribute. React uses .map() in JSX with * a key prop. * * @see ParentToChild — passing data from parent to child components via props */ import { useEffect, useState } from 'react'; import { createDataSDK, gql } from '@salesforce/platform-sdk'; const QUERY = gql` query AccountList { uiapi { query { Account(first: 6, orderBy: { Name: { order: ASC } }) { edges { node { Id Name @optional { value } Industry @optional { value } } } } } } } `; interface QueryResponse { uiapi: { query: { Account: { edges: Array<{ node: { Id: string; Name: { value: string | null }; Industry: { value: string | null }; }; }>; }; }; }; } interface AccountFields { id: string; name: string; industry: string | null; } export default function ListOfAccounts() { const [accounts, setAccounts] = useState(); const [error, setError] = useState(); useEffect(() => { const fetchAccounts = async () => { const sdk = await createDataSDK(); const result = await sdk.graphql?.query({ query: QUERY }); if (result?.errors?.length) { throw new Error( result.errors.map((e: { message: string }) => e.message).join('; ') ); } const edges = result?.data?.uiapi?.query?.Account?.edges ?? []; setAccounts( edges .map(edge => edge?.node) .filter(Boolean) .map(node => ({ id: node.Id, name: node.Name?.value ?? 'Unknown', industry: node.Industry?.value ?? null, })) ); }; fetchAccounts().catch(err => { setError(err instanceof Error ? err.message : 'Request failed'); }); }, []); if (error) return

{error}

; if (!accounts) return

Loading…

; return (
    {/* .map() replaces LWC's for:each directive. The key must be stable — record Id is ideal. */} {accounts.map(account => (
  • {account.name}

    {account.industry && (

    {account.industry}

    )}
  • ))}
); } ``` **tip:** For more examples, see the [Multi-framework recipes repo](https://github.com/trailheadapps/multiframework-recipes). ## Example: Successful GraphQL Query Response A successful query response includes record data in the `node` object. ```json { "data": { "uiapi": { "query": { "Account": { "edges": [ { "node": { "Id": "001xx000003DHP0AAO", "Name": { "value": "Acme Corp" }, "Industry": { "value": "Technology" } } } ], "pageInfo": { "hasNextPage": false, "endCursor": null } } } } } } ``` ## Example: Partial Success Response A partially successful query response includes errors and some data. ```json { "data": { "uiapi": { "query": { "Account": { "edges": [ { "node": { "Id": "001xx000003DHP0AAO", "Name": { "value": "Acme Corp" } } } ] } } } }, "errors": [ { "message": "Insufficient access on field AnnualRevenue", "path": ["uiapi", "query", "Account", "edges", 0, "node", "AnnualRevenue"] } ] } ``` ## See Also - [Error Handling in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-graphql-error.md) - [Cache Control in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-cache-control.md) - [GraphQL Queries in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-graphql.md) - _GraphQL API Developer Guide_: [Query Objects Examples](https://developer.salesforce.com/docs/platform/graphql/guide/query-record-examples.html) # Integrate Agentforce Conversation Client in Your React App Use the Agentforce Conversation Client (ACC) to integrate conversational AI interactions into your React apps for employee use cases. The ACC package is built on the Lightning Out 2.0 platform as a Lightning Web Component Interface (LWCI). With this architecture, you can embed and render the conversational UI within non-LWC tech stacks, such as a React host environment. ## Why Use ACC in React? Agentforce Conversation Client gives users immediate access to agent-driven insights, such as high-demand property data or real-time sales metrics, directly within their web dashboard. When you embed the Agentforce Conversation Client, users can retrieve complex information through natural conversation instead of manually researching vast amounts of data. - **Reduce UI Rebuild Effort**: Drop a prebuilt ACC UI container into your app instead of coding an interface and connection logic from scratch. - **Leverage Built-in Streaming Infrastructure**: ACC provides the complex infrastructure required to handle continuous, real-time token streaming for a responsive conversational experience. - **Render Rich Components Dynamically**: ACC automatically renders rich UI components that use standard and custom Lightning Types within the conversation. - **Support Personalization at Runtime**: ACC automatically interprets the Employee Agent's response and renders the correct UI component on the fly without hard-coding. - **Enable Write Once, Deploy Anywhere**: Support a consistent experience whether the Employee Agent is accessed by an employee in Service Cloud, a customer on a public website, or embedded in a custom app. - **Direct Agent Connection**: Seamlessly connect your custom UI to Employee Agents configured with specific topics, actions, and instructions for domain-specific tasks. ### Key Capabilities - **Token-by-Token Streaming**: Handles end-to-end token streaming for various content types, displaying true progress status updates in the ACC UI to indicate the current execution step, to reduce perceived latency. - **Conversation Entry Point**: Provides a default Floating Action Button (FAB) experience that serves as the primary entry point for the Agentforce Conversation Client. Upon interaction, it initializes the conversational interface and displays a configured welcome message confirming a secure connection to the Agentforce Employee Agent. ![React Start](https://developer.salesforce.com/docs/media/react-acc3.png '{"class": "image-md"}') ![React Start](https://developer.salesforce.com/docs/media/react-acc1.png '{"class": "image-md"}') - **Lightning Types Integration**: Automatically maps Salesforce Lightning Types to the correct UI renderers for input, output, list, and picklist. This allows it to render complex, interactive UI components, such as flight bookings or property listings, directly within the React application. For more information about Lightning Types, see [Lightning Types in Agent Action](https://developer.salesforce.com/docs/ai/agentforce/guide/lightning-types-standard.html). ![React Start](https://developer.salesforce.com/docs/media/react-acc2.png '{"class": "image-md"}') - **Dynamic Rendering Engine**: Constructs UI components in real time based on inputs from the Agentforce Employee Agent planner and Lightning type responses. The engine dynamically assembles elements on the fly and injects them into the designated panel. - **Brand Styling and Theming**: Provides customizable brand styling hooks (fonts, colors, borders, spacing) and component substitutions to match your custom branding. - **Customizable UI Branding via SDK Tokens**: Enables developers to use Agentforce Vibes and natural language prompts to refine the conversational interface. This capability uses the ACC SDK’s UI style tokens to update elements, such as header colors and message block styling, to match the host application's branding. - **Responsive Display Configurations (Inline Mode)**: Provides a highly responsive interface across various dashboard layouts and supports specialized configurations, such as Inline Mode, which renders the ACC UI inside a designated parent container within the internal React app. ## Determine Your Integration Path If you're building an internal, employee-facing (B2E) React app for an Employee Agent that's hosted on Salesforce, you can add ACC manually or use Agentforce Vibes to speed up the process. Alternatively, if you're: - Building an external-facing React app for Service Agent, use Enhanced Chat v2. To learn more, see [What's Enhanced Chat v2](https://help.salesforce.com/s/articleView?id=service.enhanced_chat_v2_intro.htm&type=5) in Salesforce Help. - Building a standalone web application hosted outside of Salesforce, refer to the [Agentforce Conversation Client Developer Guide](https://developer.salesforce.com/docs/platform/accsdk/overview). - Not building a React app and only need to programmatically open or close the native side panel from a custom Lightning web component (LWC) inside Salesforce, refer to the [Agentforce Conversation Client API](https://developer.salesforce.com/docs/platform/accsdk/guide/acc-api.html). ## Add ACC to Your React App with Agentforce Vibes With Agentforce Vibes, you can generate and refine conversational interfaces using natural language prompts. Agentforce Vibes uses rules and skills to understand your intent, automatically updates project files such as `index.tsx`, and manages npm dependency installations. For more information, see [Add ACC to Your React App with Agentforce Vibes](https://developer.salesforce.com/docs/platform/agentforcevibes/guide/afv-reactdev-acc.html) in the _Agentforce Vibes Developer Guide_. ## Add ACC to Your React App Manually To implement the ACC interface manually, use the `@salesforce/agentforce-conversation-client` npm package. ### Implement the React Host Component Manual integration requires invoking the `createAccWidget` method. This method wraps the ACC widget with Lightning Out 2.0 and injects the conversational interface into a designated DOM container. Implement the code using a standard React reference (`useRef`) and a lifecycle hook (`useEffect`) to manage the mounting and unmounting of the widget relative to the React component tree. ```javascript import { useEffect, useRef } from "react"; import { embedAgentforceClient } from "@salesforce/agentforce-conversation-client"; export function AgentforceConversation() { const containerRef = useRef < HTMLDivElement > null; useEffect(() => { const container = containerRef.current; if (!container) return; embedAgentforceClient({ container, frontdoorUrl: "https://...", // when embedding outside Salesforce / no org session agentforceClientConfig: { agentId: "YOUR_AGENT_ID", // required agentLabel: "Support", // optional renderingConfig: { mode: "floating" }, // or "inline" + width/height }, }); return () => { container.replaceChildren(); }; }, []); // For mode: "inline", give the wrapper a real height (e.g. flex parent or fixed height). return
; } ``` # Localize Your App Get the logged-in user's Salesforce locale context from the `i18n` module. With locale-aware formatters, the dates, numbers, and currency in your web app follow the user's Salesforce Locale setting instead of the browser default. To localize your app, import the locale context and formatters from `@salesforce/platform-sdk/i18n`. ```typescript import { createDataSDK } from "@salesforce/platform-sdk/data"; import { fetchI18nContext, createI18nFormatters } from "@salesforce/platform-sdk/i18n"; const dataSdk = await createDataSDK(); // Resolve the user's locale context, then build formatters bound to it const ctx = await fetchI18nContext(dataSdk); const { formatDate, formatNumber, formatCurrency } = createI18nFormatters(ctx); formatDate(new Date()); // returns the formatted date based on the user's locale formatNumber(1234567.89); // grouped per the user's locale formatCurrency(1000); // uses the org currency from the context ``` The i18n context is fetched over GraphQL only. The i18n runtime requires Salesforce API version 68.0 or later. | Export | Type | Description | | :------------------------- | :--------------- | :------------------------------------------------------------------------------------------- | | `fetchI18nContext` | `async function` | Resolves the user's locale context over GraphQL and caches it for the page | | `reloadI18nContext` | `async function` | Clears the cached context and re-queries it, for in-session locale changes | | `createI18nFormatters` | `function` | Builds locale-aware date, number, and currency formatters bound to a resolved context | | `createSalesforceDetector` | `function` | Builds a language detector that reports the resolved language, for use with i18next | | `SalesforceBackend` | `class` | An i18next backend plugin that fetches your custom labels over GraphQL, for use with i18next | | `SalesforceBackendOptions` | `interface` | The options passed to `SalesforceBackend` (Data SDK, label manifest, fallback strategy) | | `LabelFallback` | `type` | How the server resolves a label with no translation for the requested locale | | `I18nContext` | `interface` | The resolved locale context (`lang`, `locale`, `dir`, `currency`, `timeZone`) | | `I18nFormatters` | `interface` | The set of formatting functions returned by `createI18nFormatters` | ## I18nContext Interface The resolved locale context for the logged-in user. ```typescript interface I18nContext { lang: string; locale: string; dir: string; currency: string; timeZone: string; } ``` | Property | Type | Description | | :--------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `lang` | `string` | The translation language, such as `en`, which is used to resolve translated text. | | `locale` | `string` | The formatting locale as a BCP 47 tag, such as `en-US` or `en-GB`, which is used for formatting. It carries the region that decides date order and digit grouping. A user can share a language but differ in locale, for example `lang: "en"` with `locale: "en-GB"`. | | `dir` | `string` | The text direction, such as `ltr` or `rtl`. | | `currency` | `string` | The org's currency code, such as `USD`. The currency tracks the org setting and can differ from the locale's region. | | `timeZone` | `string` | The user's time zone, such as `America/Los_Angeles`. | ## fetchI18nContext(dataSDK) Resolves the logged-in user's locale context over GraphQL. The context is cached for the lifetime of the page, so repeated calls return the same value without re-querying. To re-query after an in-session locale change, use [`reloadI18nContext`](#reloadi18ncontextdatasdk) with a Data SDK instance. If the surface has no GraphQL support, the returned promise rejects. ```typescript function fetchI18nContext(dataSDK: DataSDK): Promise; ``` ## reloadI18nContext(dataSDK) Clears the cached context and immediately re-queries the platform, resolving to the fresh context. Because the context is cached for the lifetime of the page, a locale change otherwise takes effect only on a full page reload. Call this after an in-session locale change with a Data SDK instance. If the surface has no GraphQL support, the returned promise rejects. ```typescript function reloadI18nContext(dataSDK: DataSDK): Promise; ``` **note:** The cache is cleared before the re-query runs. If the re-query fails, the returned promise rejects and the context is left empty until a later successful fetch. Handle rejection rather than assume a reload always repopulates the context. ## createI18nFormatters(ctx) Builds locale-aware formatters bound to a resolved [`I18nContext`](#i18ncontext-interface). This factory is synchronous and takes an already-fetched context, so call [`fetchI18nContext`](#fetchi18ncontextdatasdk) first. The formatters are built from `ctx.locale`, the formatting axis, not `ctx.lang`, the translation axis. Each formatter derives its defaults from the context: - `formatDate` uses the locale and the time zone. - `formatNumber` uses the locale. - `formatCurrency` uses the locale and the currency. To override a default, pass options for that call. The options you pass are merged last. ```typescript function createI18nFormatters(ctx: I18nContext): I18nFormatters; ``` `I18nContext` is the resolved context to format against. Locale, currency, and time zone are read from it as defaults. ### I18nFormatters Interface ```typescript interface I18nFormatters { formatDate(value: Date | number, options?: Intl.DateTimeFormatOptions): string; formatNumber(value: number, options?: Intl.NumberFormatOptions): string; formatCurrency(value: number, options?: Intl.NumberFormatOptions): string; } ``` | Method | Description | | :-------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `formatDate(value, options?)` | Formats a `Date` or timestamp using the context's locale and time zone. Pass `Intl.DateTimeFormatOptions` to override the defaults for a call. | | `formatNumber(value, options?)` | Formats a number using the context's locale. Pass `Intl.NumberFormatOptions` to override the defaults for a call. | | `formatCurrency(value, options?)` | Formats a number as currency using the context's locale and currency. Pass a `currency` in `Intl.NumberFormatOptions` to override the context's currency for a call. If neither the context nor the options supply a currency code, `formatCurrency` throws. | ```typescript const ctx = await fetchI18nContext(dataSdk); const { formatDate, formatCurrency } = createI18nFormatters(ctx); // Use the context defaults formatDate(new Date()); // Override per call formatDate(new Date(), { dateStyle: "full" }); formatCurrency(1000, { currency: "EUR" }); ``` **note:** The formatters are separate from label text. If a label uses positional placeholders (`{0}`, `{1}`), substituting a raw `Date` or number inserts its default `toString()`, not a locale-aware value. To interpolate a localized number or date, format it with `createI18nFormatters` first and pass the resulting string as the placeholder value. ## createSalesforceDetector(dataSDK) Builds a language detector that reports the resolved language (`I18nContext.lang`). The returned object is structurally compatible with the i18next language-detector plugin slot, so you can pass it to i18next's `.use()` if your app uses i18next for translations. The detector reports the language only after a context has been resolved with [`fetchI18nContext`](#fetchi18ncontextdatasdk). ```typescript function createSalesforceDetector(dataSDK: DataSDK): LanguageDetectorModule; ``` ## SalesforceBackend An [i18next](https://www.i18next.com) backend plugin that fetches your org's custom labels over GraphQL at runtime and hands them to i18next to render. Use `SalesforceBackend` when your app translates UI text with i18next. Register it as an i18next backend and give it the list of labels your app uses. The class is structurally compatible with i18next's backend plugin slot, so you pass it to i18next's `backend` configuration. `platform-sdk` does not take a hard dependency on i18next. ```typescript import { createDataSDK } from "@salesforce/platform-sdk/data"; import { SalesforceBackend } from "@salesforce/platform-sdk/i18n"; import i18next from "i18next"; const dataSDK = await createDataSDK(); await i18next.use(SalesforceBackend).init({ defaultNS: "c", // "c" is your org's custom-label namespace backend: { dataSDK, labelManifest: ["c:Welcome_Text", "c:Save_Button"], }, interpolation: { // Salesforce labels interpolate with {0}, {1}, … — map i18next to that syntax prefix: "{", suffix: "}", }, }); ``` The backend reads the label manifest, groups the entries by namespace, deduplicates the names, and fetches them over GraphQL. A namespace with more than 100 labels is split into batches automatically, so your manifest can list any number of labels without extra configuration. A label whose value doesn't resolve is left out of the result, so the i18n library falls back to its own key-name fallback for that key. The label runtime path requires Salesforce API version 68.0 or later. On an earlier org, labels don't resolve and the affected text renders as its raw key name. ### SalesforceBackendOptions The options object passed to `SalesforceBackend` through i18next's `backend` configuration. ```typescript interface SalesforceBackendOptions { dataSDK: DataSDK; labelManifest?: string[]; labelFallback?: LabelFallback; } ``` | Property | Type | Description | | :-------------- | :-------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `dataSDK` | `DataSDK` | The Data SDK instance whose GraphQL surface fetches the labels. | | `labelManifest` | `string[]` | The labels your app uses, each as `"namespace:Key"` (for example `"c:Welcome_Text"`). `c` is your org's custom-label namespace. Labels not listed here aren't fetched. | | `labelFallback` | `LabelFallback` | How the server resolves a label with no translation for the requested locale. Defaults to `BASE_VALUE`. | ### LabelFallback Sets how the server resolves a label that has no translation for the requested locale. ```typescript type LabelFallback = "BASE_VALUE" | "USER_DEFAULT" | "NONE"; ``` - `BASE_VALUE`—Returns the org-default (base) value. This is the default. - `USER_DEFAULT`—Falls back to the logged-in user's language first, then the base value. - `NONE`—Doesn't fall back. A label with no translation for the locale is left unresolved and is absent from the result. ## See Also - [Work with Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-intro.md) - [Get Custom Labels](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-extension-label.md) # Make an Embedded App Accessible to Screen Reader Users Give an embedded app an accessible name so screen reader users understand what the embedded region contains. The embedding component exposes this name to assistive technology, and screen readers announce it when users reach the embedded region. ## How It Works Salesforce renders your embedded app inside an iframe. An iframe needs an accessible name so that screen readers can announce the region instead of leaving it unlabeled. You set a descriptive name on the embedding component, and Salesforce applies it to the frame. **note:** The generally available embedding component passed accessibility review with no violations. Focus moving into and out of the iframe is handled natively by the browser through standard iframe tabbing. The component doesn't add a custom focus manager. **note:** Salesforce gives the embedded frame an accessible name and relies on native browser focus, but accessibility of the app _inside_ the iframe is your responsibility as the app owner. Make sure your app's own semantics are accessible: headings, ARIA, keyboard operability, color contrast, and applying the forwarded `lang` and `dir` attributes. ## Prerequisites - Your app is embedded with `lightning-ui-embedding` and loads successfully. ## Set the Accessible Name Set a descriptive name on the embedding component. Screen readers announce this name when users reach the embedded region. ```html ``` Use a name that describes the app's purpose, such as "Loan application" or "Order status", so users know what the region does before they enter it. If you don't set `title`, the frame uses the default accessible name `Embedded widget`. Set a specific `title` so screen reader users hear a name that reflects your app. ## See Also - [Get Started with UI Embedding](https://developer.salesforce.com/docs/platform/microfrontend/guide/get-started.md) # Make an Embedded App Accessible to Screen Reader Users (Developer Preview) Give an embedded app an accessible name so screen reader users understand what the embedded region contains, and let users move into and out of the app with the keyboard. **note:** This topic describes the Developer Preview API. The accessible name is set through `shell.title`. In the generally available release, this becomes a plain `title` attribute on the `` component. To move an app to the generally available version, see [Migrate from the Developer Preview API](https://developer.salesforce.com/docs/platform/microfrontend/guide/migrate-from-dev-preview.md). ## How It Works Salesforce renders your embedded app in an iframe. An iframe needs an accessible name so screen readers can announce the region instead of leaving it unlabeled. You set a descriptive name on the shell, and the shell applies it to the iframe as its title. Keyboard users tab into and out of the embedded app in natural document order, using the browser's built-in iframe tabbing. ## Set the Accessible Name Set a descriptive name on the shell's `title` property. Screen readers announce this name when users reach the embedded region. ```javascript shell.title = "Loan application"; ``` Use a name that describes the app's purpose, such as "Loan application" or "Order status", so users know what the region does before they enter it. ## Keyboard Navigation Users move through the embedded app in natural tab order. The host relies on the browser's native iframe tabbing rather than a custom focus manager, so focus flows into the app and back out to the page as users tab. ## See Also - [Embed an External Web App (Developer Preview)](https://developer.salesforce.com/docs/platform/microfrontend/guide/dp-embed-external-web-app.md) - [Warn Users About Unsaved Changes in an Embedded App (Developer Preview)](https://developer.salesforce.com/docs/platform/microfrontend/guide/dp-track-unsaved-changes.md) # Manage the Host UI Use the View SDK to interact with the host's UI layer from a UI bundle. The host is the application that embeds and runs your UI bundle, such as a parent Lightning web component that embeds your app or an agent host. The View SDK is how your app coordinates with that host. With the View SDK, you can display alerts, toasts, and modals, read and subscribe to host UI state, For example, you can also get the current theme, mark unsaved changes, exchange events with the host, and more. Create an instance with `createViewSDK()`, then call its methods under the returned SDK. ```typescript import { createViewSDK } from "@salesforce/platform-sdk"; const viewSDK = await createViewSDK(); await viewSDK.displayToast?.({ message: "Operation completed successfully", level: "success", }); ``` Every method is optional and resolves only on surfaces that support it. On web apps without an embedding host, `createViewSDK()` returns an empty SDK and doesn't return an error. Use optional chaining (`viewSDK.displayToast?.(...)`) to account for the absence of a host UI. ## createViewSDK(options?) Creates and initializes a View SDK instance. ```typescript async function createViewSDK(options?: ViewSDKOptions): Promise; ``` ### Parameters | Parameter | Type | Description | | :--------- | :--------------- | :---------------------------------------------------------------------- | | `options?` | `ViewSDKOptions` | Optional configuration. Extends `SDKOptions` with a `surface` override. | ### Returns ```typescript Promise ``` `createViewSDK()` never rejects on an unusable surface. Every surface degrades to an empty SDK so that the embedding host always renders. ## Get a Shared Instance To reuse one View SDK instance across your app, use `getViewSDK()`. It creates the instance on the first call and returns the same cached instance on later calls. Only the first call's `options` take effect. ```typescript import { getViewSDK } from "@salesforce/platform-sdk"; const viewSDK = await getViewSDK(); ``` Where an async call isn't available, use `getViewSDKSync()`. It returns the resolved instance if `getViewSDK()` has already completed, or `null` if the instance isn't ready. ```typescript import { getViewSDKSync } from "@salesforce/platform-sdk"; const viewSDK = getViewSDKSync(); if (viewSDK) { const theme = viewSDK.getTheme?.(); } ``` ## ViewSDK Interface The API returned by `createViewSDK()`. All methods are optional and are absent on a surface that doesn't support them. The View SDK is also a DOM `EventTarget`, so it optionally exposes `addEventListener`, `removeEventListener`, and `dispatchEvent`. | Method | Description | | :---------------- | :------------------------------------------------------------------------------------------------------ | | `displayAlert` | Displays a modal alert that requires user acknowledgment. Use it for important messages. | | `displayToast` | Displays a non-blocking toast that auto-dismisses. Use it for confirmations and status updates. | | `displayModal` | Displays a modal dialog with custom HTML loaded from a UI resource. | | `navigateTo` | Navigates to a URL in the host environment. | | `markDirtyState` | Notifies the host that the app has unsaved changes. | | `clearDirtyState` | Notifies the host that all changes are saved. | | `getUiState` | Returns the current host UI state snapshot and a `subscribe` function for future changes. | | `getTheme` | Returns the current theme (`light` or `dark`), or `null` if the host doesn't provide theme information. | | `resize` | Requests the host to resize the app container. | ## displayAlert() Displays a modal alert in the host environment. Alerts block interaction until the user acknowledges them. ```typescript displayAlert?(options: AlertOptions): Promise; ``` `AlertOptions` has a required `message` string and an optional `level` of `"info"`, `"success"`, `"warning"`, or `"error"`. The `level` defaults to `"info"`. ```typescript await viewSDK.displayAlert?.({ message: "Save your work before continuing.", level: "warning", }); ``` ## displayToast() Displays a non-blocking toast notification that dismisses automatically. ```typescript displayToast?(options: ToastOptions): Promise; ``` `ToastOptions` has the same shape as `AlertOptions`: a required `message` and an optional `level`. ```typescript await viewSDK.displayToast?.({ message: "File uploaded successfully.", level: "success", }); ``` ## displayModal() Displays a modal dialog with custom HTML loaded from a UI resource. ```typescript displayModal?(options: ModalOptions): Promise; ``` | Parameter | Type | Description | | :------------------- | :------------------------ | :------------------------------------------------------------------------------------ | | `componentReference` | `string` | URI of the HTML template to load, in the format `ui://widget/[name].html`. | | `params` | `Record` | Optional. Parameters passed to the modal template, accessible through the host's API. | ```typescript await viewSDK.displayModal?.({ componentReference: "ui://widget/settings.html", params: { theme: "dark" }, }); ``` ## navigateTo() Navigates to a URL in the host environment. The exact behavior depends on the surface, such as a full-page navigation or an iframe navigation. ```typescript navigateTo?(url: string): Promise; ``` ```typescript await viewSDK.navigateTo?.("/dashboard"); ``` ## markDirtyState() Notifies the host that the app has unsaved changes. When the state is dirty, the host can show an indicator and warn the user before they navigate away. Pass an optional `label` to track multiple concurrent dirty regions. ```typescript markDirtyState?(label?: string): Promise; ``` ```typescript form.addEventListener("input", () => viewSDK.markDirtyState?.("PrimaryForm")); ``` ## clearDirtyState() Notifies the host that the app's unsaved changes are resolved. Pass the same `label` that you passed to `markDirtyState()` to clear that dirty region. ```typescript clearDirtyState?(label?: string): Promise; ``` ```typescript async function saveForm(data) { await api.save(data); await viewSDK.clearDirtyState?.("PrimaryForm"); } ``` ## getUiState() Returns the current host UI state snapshot and a `subscribe` function for future changes. The snapshot has `props`, the host-configurable inputs such as theme, locale, and mode, and `styles`, the CSS custom properties and attributes the host mirrors onto the surface. Call `subscribe` with a handler to be notified on each later host push. `subscribe` returns an `unsubscribe` function that detaches that one handler. ```typescript getUiState?(): { state: State; subscribe: (handler: (state: State) => void) => () => void; }; ``` ```typescript const { state, subscribe } = viewSDK.getUiState?.() ?? { state: { props: {}, styles: {} }, subscribe: () => () => {}, }; applyProps(state.props); const unsubscribe = subscribe((next) => applyProps(next.props)); // Call unsubscribe() when you no longer need updates. ``` To type the snapshot for a known host contract, pass a type that extends `UiState` as the type parameter. `State` is erased at runtime, so pin only what the host guarantees. ## getTheme() Returns the current theme, or `null` if the host doesn't provide theme information. `getTheme()` is the synchronous sibling of the theme data in `getUiState()`. ```typescript getTheme?(): Theme | null; ``` ```typescript const theme = viewSDK.getTheme?.(); if (theme) { document.body.classList.toggle("dark", theme.mode === "dark"); } ``` ### resize() Requests the host to resize the app container. Pass a width and a height as CSS length strings, or `"auto"` to size to the content. ```typescript resize?(width: string, height: string): Promise; ``` ```typescript await viewSDK.resize?.("800px", "600px"); await viewSDK.resize?.("auto", "auto"); ``` ## Exchange Events with the Host The View SDK is a DOM `EventTarget`. On surfaces backed by a host bridge, `dispatchEvent` notifies local listeners and forwards the event to the host, and events the host pushes are re-dispatched onto the SDK so `addEventListener` hears them. The result is one unified stream of host-pushed and same-document events. The `EventTarget` members are optional, because a surface without an event bus is a bare SDK. Feature-detect before you use them. ```typescript viewSDK.addEventListener?.("user-action", (e) => { console.log("received:", (e as CustomEvent).detail); }); viewSDK.dispatchEvent?.(new CustomEvent("user-action", { detail: { action: "click" } })); ``` ## Example: React to the Host Theme in a React Component This example reads the theme inside an effect and subscribes to later changes. Guard the async result against a component that unmounts before the promise settles, and detach the subscription on cleanup. ```typescript import { useEffect, useState } from "react"; import { createViewSDK, type ThemeMode } from "@salesforce/platform-sdk"; function useHostTheme(): ThemeMode { const [mode, setMode] = useState("light"); useEffect(() => { let cancelled = false; let unsubscribe: (() => void) | undefined; createViewSDK().then((viewSDK) => { if (cancelled) return; const initial = viewSDK.getTheme?.(); if (initial) setMode(initial.mode); const uiState = viewSDK.getUiState?.(); unsubscribe = uiState?.subscribe((next) => { const themeMode = next.props.theme; if (themeMode === "light" || themeMode === "dark") setMode(themeMode); }); }); return () => { cancelled = true; unsubscribe?.(); }; }, []); return mode; } ``` ## See Also - [Work with the Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-intro.md) - [App Identity](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-identity.md) - [Style Your React Apps](https://developer.salesforce.com/docs/platform/multiframework/mfw-styling.md) # Manage Your App and Grant User Access After you deploy a Multi-Framework app, you manage it in Setup from App Manager, the same place you manage any Lightning app. Both internal and external apps appear there, listed as Multi-Framework in the Type column. How you grant users access depends on the app. An internal app opens in Salesforce, and you control who can see it in the App Launcher with a permission set or profile, the same as any custom app. An external app runs on an Experience Cloud site and is visible to everyone by default, like any site, so you control access through the site's guest and authenticated-user settings. An external app also appears in the App Launcher, but selecting it opens the site's URL in a new tab. **note:** The access steps in this topic are for internal apps, which users open from the App Launcher. You still manage external apps in App Manager, but users reach them through an Experience Cloud site, so you control access with the site's guest and authenticated-user settings instead. ## Manage Your App in Setup Both internal and external Multi-Framework apps appear in App Manager, listed as Multi-Framework in the Type column. Standard Experience Cloud sites built with LWR or Aura appear as the Community type instead, so the Multi-Framework type identifies your Multi-Framework apps. 1. From Setup, in the Quick Find box, enter `App Manager`, and then select **App Manager**. 2. Find your app in the list, and then select **View** from the row menu. Whether an app has editable details depends on how it's hosted: - An internal app is registered as a CustomApplication and has an App Details page. From it, you can edit the app's label and description. Other settings are defined in your app's metadata. - An external app that runs on an Experience Cloud site doesn't have an App Details page. You manage it through its site instead. The app connects to your frontend code through the `uiBundle` reference in its metadata, which points to your UIBundle by its fully qualified name, such as `c__MyApp`. **note:** Structural details like the `uiBundle` reference are defined in your app's metadata. To change them, edit the metadata in your Salesforce DX project and redeploy rather than editing in Setup, so that your source stays the source of truth. See [Deploy and Publish a UIBundle](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-deploy.md). ## Grant Users Access to Your App Access to a Multi-Framework app works the same way as access to any Lightning app: a user can open the app when a permission set or profile makes the app visible to them. Because your app reads and writes Salesforce data through the platform API, users also need the **API Enabled** permission. Granting access takes two things that are easy to confuse: making the app _visible_ to a user, and giving that user access to the _data_ the app reads and writes. The generated permission set covers app visibility and API access. Object- and field-level data access can come from the same permission set or from the user's profile or another permission set. A user who can open the app but has no access to its objects sees an empty or partial UI, not an error. **note:** Deploying a permission set doesn't assign it. When you deploy your project, the ` Access` permission set is created in the org, but no user has it until you assign it. Assign the permission set after you deploy. The internal app templates generate a permission set that grants both app visibility and API Enabled, so assigning it is the fastest way to grant access. ### Assign the Generated Permission Set The internal templates include a permission set named **` Access`** (for example, **MyApp Access**). It sets the app to visible and enables the API Enabled user permission. Assign it to a user with the Salesforce CLI: ```bash sf org assign permset --name MyApp_Access ``` Or assign it from Setup: 1. From Setup, in the Quick Find box, enter `Permission Sets`, and then select **Permission Sets**. 2. Select the **` Access`** permission set. 3. Select **Manage Assignments**, and then **Add Assignments**. 4. Select the users, and then select **Assign**. ### Grant Access Through a Profile If you manage access with profiles instead of permission sets, make the app visible to a profile: 1. From Setup, open **App Manager**, and edit your app. 2. On the **User Profiles** step, select the profiles that can access the app, and then save. Confirm that those profiles also have the **API Enabled** permission so the app can load data. ## Open the App from the App Launcher After a user has access, they open an internal app from the App Launcher. 1. Select the App Launcher (the grid icon) in the navigation bar, on desktop or in the Salesforce mobile app. 2. Search for the app by name. 3. Select the app. It opens at its `salesforce.app` domain URL in a new browser tab. External apps also appear in the App Launcher. Because they run on an Experience Cloud site, selecting one opens the site's URL in a new browser tab instead of opening in Salesforce. If an internal app doesn't appear, confirm that: - The UIBundle's `target` is `CustomApplication`, and the CustomApplication's `uiBundle` field references the UIBundle. - The user has the **` Access`** permission set or a profile that makes the app visible. - The UIBundle is deployed and its `isActive` flag is `true`. If the app opens but its data doesn't load, the user has app visibility but not access to the app's data. Confirm that the user has object- and field-level access to the objects the app reads and writes, through their profile or a permission set. ## See Also - [Deploy and Publish a UIBundle](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-deploy.md) - [Quick Start: Build and Run Your First App](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-quick-start.md) - _Salesforce Help_: [Permission Sets](https://help.salesforce.com/s/articleView?id=sf.perm_sets_overview.htm&type=5) - _Salesforce CLI Command Reference_: [org assign permset](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference_org_commands_unified.htm) # Match an Embedded App to the Salesforce Theme (Developer Preview) Keep an embedded app visually consistent with the surrounding page by passing Salesforce theme values into it as CSS custom properties. The `` host reads the custom properties set on it and forwards them to your app. **note:** This topic describes the Developer Preview API, including the host method `shell.refreshTheme()`. In the generally available release, theming moves to a reactive UI-state model rather than an imperative refresh call. To move an app to the generally available version, see [Migrate from the Developer Preview API](https://developer.salesforce.com/docs/platform/microfrontend/guide/migrate-from-dev-preview.md). ## How It Works When the shell mounts, it reads the CSS custom properties (the `--*` variables) set on the shell element and passes their values to your app across the iframe boundary. Your app reads those values and applies them to its own styles, so its colors and spacing follow the page's theme. The shell reads the custom properties once when it mounts. It doesn't automatically watch for later style changes on the host. If the theme changes at runtime, for example when a user switches to dark mode, call `refreshTheme()` to send the updated values. ## Set the Theme Values Set CSS custom properties on the shell element, or on an ancestor the shell inherits from. ```css lwc-shell { --brand-color: #0176d3; --background-color: #f3f3f3; } ``` The shell reads these values at mount and forwards them to your app. In your app, read the forwarded custom properties and map them to your own styles. **note:** Theming is scoped to CSS custom properties. Salesforce doesn't restyle your app's components for you. Your app decides how to apply the values it receives. ## Update the Theme at Runtime If the theme changes after the app has loaded, call `refreshTheme` on the shell to re-read the custom properties and send the new values. ```javascript shell.refreshTheme(); ``` ## See Also - [Embed an External Web App (Developer Preview)](https://developer.salesforce.com/docs/platform/microfrontend/guide/dp-embed-external-web-app.md) - [Exchange Data Between the Page and an Embedded App (Developer Preview)](https://developer.salesforce.com/docs/platform/microfrontend/guide/dp-exchange-data.md) # Migrate from the Developer Preview API UI Embedding was first released as a Developer Preview. The generally available version changes the host element, how you pass the app URL, and how the host communicates with the embedded app. If you built an embedding against the Developer Preview, use this guide to move to the generally available component. **important:** The Developer Preview and generally available APIs aren't compatible. An app built for one doesn't run under the other without the changes described here. ## What Changed | Area | Developer Preview | Generally Available | | ------------------------- | ------------------------------------- | ---------------------------------------------------------------------------- | | Host element | `` | `` | | App URL | Set through the shell's configuration | `src` attribute (required, read once at mount) | | Host-to-app communication | Imperative `bridge` object | DOM events on the host element and the `sf-embedding` protocol channel | | Lifecycle signals | `bridge` callbacks | `sf-embedding.component.ready` and `sf-embedding.component.error` DOM events | ## Update the Host Element Replace the Developer Preview host element with `lightning-ui-embedding`, and move the app URL to the `src` attribute. Before (Developer Preview): ```html ``` After (generally available): ```html ``` The `src` attribute is required and binds the session to a specific app. Salesforce reads it once when the component mounts. Don't change `src`, `sandbox`, or `removeSandboxTokens` after the component renders. The host rejects a mid-session change with a `SESSION_BINDING_MUTATED` error. ## Replace the Imperative Bridge with DOM Events The Developer Preview exposed an imperative `bridge` object for communication. The generally available version uses DOM events on the host element for lifecycle signals. Listen for these events on `lightning-ui-embedding`: - `sf-embedding.component.ready` fires when the embedded app finishes the handshake and is ready. - `sf-embedding.component.error` fires when embedding fails. The event detail includes the `phase` (`configuration` or `bootstrap`), a `code`, a `message`, and a `retryable` flag. See [UI Embedding Error Codes](https://developer.salesforce.com/docs/platform/microfrontend/references/microfrontend-error-codes/error-codes.html). These event names contain dots, so you can't bind them declaratively with `on*` attributes in the template. Add `lwc:ref` to the embedding element and attach the listeners imperatively in `renderedCallback`. ```html ``` ```javascript // myEmbeddedApp.js import { LightningElement } from "lwc"; export default class MyEmbeddedApp extends LightningElement { appUrl = "https://app.example.com"; hasWiredEvents = false; renderedCallback() { if (this.hasWiredEvents) { return; } this.hasWiredEvents = true; const el = this.refs.embedding; el.addEventListener("sf-embedding.component.ready", this.handleReady); el.addEventListener("sf-embedding.component.error", this.handleError); } handleReady = () => { // The embedded app is ready. }; handleError = (event) => { const { code, message } = event.detail; // Handle the failure. }; } ``` ## Update Data Exchange, Styling, and Resizing In the generally available version, the page and the embedded app exchange state through the embedding's typed channel rather than the Developer Preview `bridge`. Review these tasks for the current approach: - [Style an Embedded App to Match Salesforce](https://developer.salesforce.com/docs/platform/microfrontend/guide/style-embedded-app.md) - [Resize an Embedded App to Fit Its Content](https://developer.salesforce.com/docs/platform/microfrontend/guide/resize-embedded-app.md) - [Track Unsaved Changes in an Embedded App](https://developer.salesforce.com/docs/platform/microfrontend/guide/track-unsaved-changes.md) ## Next Steps - [Get Started with UI Embedding](https://developer.salesforce.com/docs/platform/microfrontend/guide/get-started.md) for the full generally available setup path. - [UI Embedding Error Codes](https://developer.salesforce.com/docs/platform/microfrontend/references/microfrontend-error-codes/error-codes.html) to handle failures in your event listeners. # Package Your App for Distribution You can distribute a Multi-Framework app as a second-generation package (2GP), which wraps your UIBundle and its related metadata into a single, versioned artifact that you install into a subscriber org, upgrade in place, and uninstall. ## What You Can Package Both package types support internal, employee-facing (B2E) apps built with React or Angular. Packaging is unsupported for external-facing (B2B and B2C) apps. Choose a package type based on how you distribute the app. - Use unlocked packages for internal distribution, proofs of concept, and rapid iteration. - Use managed packages for listing on AppExchange or customer distribution. For a full comparison of the two types, including namespaces, component editability, and AppExchange eligibility, see [Second-Generation Managed Packages](https://developer.salesforce.com/docs/platform/pkg2-dev/guide/sfdx-dev-dev2gp.html) and [Unlocked Packages](https://developer.salesforce.com/docs/platform/pkg2-dev/guide/sfdx-dev-unlocked-pkg-intro.html) in the packaging guide. ## Prepare Your UIBundle for Packaging Beyond the standard packaging setup (a Dev Hub with second-generation packaging enabled, and a registered namespace for a managed package), packaging a UIBundle has a few specific requirements. ### Set the Source API Version Set `sourceApiVersion` to `67.0` or later as a top-level property in `sfdx-project.json`, alongside `packageDirectories`. ```json { "sourceApiVersion": "67.0", "packageDirectories": [{ "path": "force-app", "default": true }] } ``` For a managed package, also set the top-level `namespace` property to your registered namespace. ### Include the App's Metadata Make sure that your package directory contains your UIBundle and its related metadata. For an internal app, include the CustomApplication type that references the UIBundle. To give subscribers a ready-made access grant, you can also include the permission set that the template generates with your app. ### Build the Bundle Build your UIBundle so the packaged assets are current before you create a package version. ```bash cd force-app/main/default/uiBundles/myapp npm install npm run build ``` **note:** Use the local dev server (`npm run dev`) as your inner development loop while you build the app. A full build, version create, and install round trip takes several minutes, so don't use `sf package version create` to iterate on UI changes. See [Preview and Run Your App Locally](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-preview.md). ## Prepare a Managed Package for AppExchange Submission Before you submit a Multi-Framework app as a managed package to AppExchange for a security review, complete these two prerequisites. Unlocked packages don't go through AppExchange Security Review and therefore don't require these steps. ### Enable Sourcemap Emission To verify that the compiled JavaScript in `dist/` traces back to your source code, the Salesforce Code Analyzer reads the sourcemaps that your build tool emits. Sourcemaps are required for the analyzer to run its UIBundle checks. In your `vite.config.ts`, set `sourcemap` to `true` in the `build` block: ```typescript export default defineConfig(({ mode }) => { return { base: "./", plugins: [ // tailwindcss(), react(), salesforce(), ... ], build: { outDir: resolve(__dirname, "dist"), assetsDir: "assets", sourcemap: true, // ← must be true for AppExchange submission }, // resolve: { ... }, // test: { ... }, }; }); ``` Verify that every `.js` file under `dist/` has a co-located `.js.map`, and that the `sources[]` array inside each map uses relative paths. Don't include absolute paths, such as `/Users/…` or `C:\…`. ### Run the Code Analyzer Scan Before submitting, run the Salesforce Code Analyzer with the `uibundle` selector to scan your compiled bundle. First, make sure that the plugin is version 5.16.0 or later: ```bash sf plugins install @salesforce/plugin-code-analyzer@latest sf plugins # confirm code-analyzer 5.16.0 or later ``` Then, from your project root after you've run `npm run build`, run the scan: ```bash sf code-analyzer run \ --rule-selector uibundle \ --rule-selector Recommended \ --output-file CodeAnalyzerReport.html ``` The `uibundle` selector runs UIBundle integrity checks, and `Recommended` runs the standard cross-engine baseline (ESLint, RetireJS, and others). The two selectors compose additively with no overlap. ## Verify the Packaged UIBundle After you install the package in a subscriber org, query it through the Tooling API to confirm that the UIBundle component is present. ```bash sf data query \ --query "SELECT Id, DeveloperName, IsActive, ManageableState, NamespacePrefix FROM UIBundle" \ --target-org my-subscriber --use-tooling-api ``` - For a managed package, `ManageableState` is `installed`, meaning it's locked, and `NamespacePrefix` is your namespace. - For an unlocked package, `ManageableState` is `installedEditable`. - In both cases, `IsActive` is `true`. Then, to confirm that the app renders, open the app from the App Launcher in the subscriber's org. **note:** Because an upgrade doesn't overwrite the UIBundle's `IsActive` value, you must activate the new version manually after you upgrade it. ## See Also - _Second-Generation Managed Packaging Developer Guide_: [UI Bundle](https://developer.salesforce.com/docs/platform/pkg2-dev/guide/packaging-packageable-components.html#ui-bundle) - _Metadata API Developer Guide_: [UIBundle](https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_uibundle.htm) - _Salesforce CLI Command Reference_: [package Commands](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference_package_commands_unified.htm) - [Project Structure and Metadata](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-project-structure.md) - [Deploy and Publish a UIBundle](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-deploy.md) # Per-Request Headers GraphQL requests in the Data SDK uses HTTP POST to a `/graphql` endpoint. Request headers are key-value metadata sent in an HTTP request. For multi-framework apps, these default headers apply to every `query()` and `mutate()` requests. - [`Content-Type: application/json`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Type) - [`Accept: application/json`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept) - `X-Chatter-Entity-Encoding: false` Multi-framework apps include the `X-Chatter-Entity-Encoding: false` header, which disables HTML entity encoding of string fields in the response. To provide your own header on a per-request basis for `query()` or `mutate()`, pass in the optional `headers` parameter on a single request. If you pass in a duplicate header that collides with a default header, such as `Content-Type`, your header value replaces the default value. ```typescript const dataSdk = await createDataSDK(); const result = await dataSdk.graphql?.query({ query: GET_ACCOUNTS, variables: { first: 20 }, headers: { Authorization: `Bearer ${token}`, }, }); ``` You can use `headers` to pass an authorization token, a trace ID, or another per-request header. ```typescript const dataSdk = await createDataSDK(); const result = await dataSdk.graphql?.mutate({ mutation: CREATE_ACCOUNT, variables: { input }, headers: { "X-Trace-Id": id, }, }); ``` Unique headers such as per-request trace IDs create a distinct cache entry and eliminate cache reuse. `headers` accepts any [`HeadersInit`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) value: a plain object, an array of `[name, value]` tuples, or a `Headers` instance. If you use the same header name more than once, the values for that header are merged into a single comma-separated value. The `headers` parameter applies only to HTTP requests. Review these usage considerations for `headers`. - Header names are case-insensitive. Your header can override the SDK's default of the same name, so avoid overriding transport headers such as `Content-Type` unless you intend to. - `headers` is part of the cache key. For `query()`, two calls that are identical except for their `headers` resolve to different cache entries. This behavior prevents a response that's scoped by a header, such as `Authorization`, from being served to a later caller that passed a different value. Calls with equivalent headers still share an entry. See [Cache Control in Data SDK](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk-cache-control.md). - `Accept-Language` is automatically appended. When the runtime environment provides an active language, the SDK adds an `Accept-Language` header to REST and GraphQL requests. Provide your own `Accept-Language` in `headers` only if you want to override the environment default. # Preview and Run Your App Locally As you build a Multi-Framework app, you can run it locally and preview your changes without deploying to an org each time. You have two options: run the app on a local development server in your browser, or use Live Preview to render the app inside Visual Studio Code. Both give you a fast edit-and-refresh loop, and both work whether you develop with Agentforce Vibes, an agentic IDE, or any other editor. ## Run a Local Development Server Each project template includes an npm script that starts a local development server. From your app's directory, run: ```bash npm run dev ``` The server starts at `http://localhost:5173`, and your browser reloads automatically when you save a change. `npm run dev` is the dev-server command for both React and Angular apps. To use a different port, set the `SF_UIBUNDLE_PORT` environment variable—for example, `SF_UIBUNDLE_PORT=4200 npm run dev`. For Angular apps, you can also start the server with the standard Angular CLI: ```bash npx ng serve ``` ## Preview Your App in VS Code with Live Preview The Salesforce Live Preview extension renders your running app in a Visual Studio Code tab and updates it automatically as you save source changes, so you don't manually redeploy or refresh. Live Preview connects to your authorized org, so your app previews with real org data. Before you start, install the Salesforce Extension Pack and the UI Bundle development plugin: ```bash sf plugins install @salesforce/plugin-ui-bundle-dev ``` 1. In VS Code, make sure that you've authorized the org you want to preview against. 2. Start Live Preview one of two ways: - Open the Command Palette and run **SFDX: Open in Live Preview**. - Right-click a source file (`.js`, `.jsx`, `.ts`, or `.tsx`) in the Explorer or editor and select **SFDX: Open in Live Preview**. 3. Select your app. Your app opens in a VS Code tab. As Agentforce Vibes or you save changes to the source, the preview refreshes. **note:** If your app embeds the Agentforce Conversation Client, register your local preview origin (for example, `http://localhost:5173`) in the Trusted Domains for Inline Frames registry in Session Settings. See [Set Up Your Org for Multi-Framework App Development](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-setup.md). ## Local Development Server or Live Preview Both approaches support rapid iteration. Choose based on where you want to see your app and what data it needs. | Consideration | Local development server | Live Preview | | ------------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------- | | **Where the app renders** | Your browser, at `http://localhost:5173` | A tab inside Visual Studio Code | | **Data** | Your app's local or mocked data, plus any org connection your app code makes | Your authorized Salesforce org | | **Best for** | Fast UI iteration and framework tooling (hot reload, framework dev server features) | Previewing against real org data as you build | ## See Also - [Deploy and Publish a UIBundle](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-deploy.md) - [Visual Marketplace: Salesforce Live Preview](https://marketplace.visualstudio.com/items?itemName=salesforce.salesforcedx-vscode-ui-preview) # Project Structure and Metadata A Multi-Framework app lives in your Salesforce DX project as a UIBundle. The UIBundle represents your app's UI and works with several other metadata types to define the app on the platform. In your project, its source code, build configuration, and metadata definition are stored together in a self-contained folder. When you deploy it, this metadata creates a UIBundle record that establishes the app's identity on the Salesforce Platform and declares which containers can host it. This topic describes the files and metadata that make up an app, whichever framework you use. ## UIBundle Files The Salesforce DX project structure includes a `force-app/main/default` folder that contains a `uiBundles` folder. Application metadata and content live together in this `uiBundles` directory. Each application subdirectory is a complete, self-contained unit with its own metadata definition (`.uibundle-meta.xml`), runtime configuration (`ui-bundle.json`), source code, and compiled assets. ```shell my-sfdx-project └── force-app/ └── main/ └── default/ └── uiBundles/ └── myapp/ ├── myapp.uibundle-meta.xml # Salesforce metadata ├── ui-bundle.json # Runtime configuration ├── package.json # Application dependencies ├── src/ # Application source code │ ├── components/ # UI components │ ├── pages/ # Application pages │ ├── assets/ # Images, fonts, icons │ └── styles/ # Global styles └── dist/ # Build output (generated) ├── sfdx-project.json # SFDX project configuration └── package.json # Root workspace configuration ``` Each app includes framework build files that connect your app to the platform: - `vite.config.ts` for React apps - `angular.json` and its `esbuild/` and `middleware/` folders for Angular apps **note:** The UIBundle metadata type can contain up to 2,500 files. For more information, see [Salesforce DX Project Structure and Source Format](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_source_file_format.htm). ## The .uibundle-meta.xml File The `.uibundle-meta.xml` file, located in the `force-app/main/default/uiBundles/{app_name}` folder, defines the metadata for the app's UI. The `target` property in this file declares which kind of Salesforce container can use this UIBundle for its UI. It doesn't surface anything on its own. Instead, a CustomApplication or a site references the UIBundle, and that container is what appears in the Salesforce UI. - `CustomApplication` lets a CustomApplication, which is an internal, employee-facing app, use the UIBundle. This is the default target if you don't specify a value. The CustomApplication appears in the App Launcher and opens in Salesforce, rendering its UI from the UIBundle. - `Experience` lets an Experience Cloud site use the UIBundle. The site appears in the Digital Experiences app and in the App Launcher, where selecting it opens the site URL in a new tab, and renders its UI from the UIBundle. **important:** In both cases, additional metadata types are required to integrate your app with the Salesforce Platform and make it available in the UI. For example, to make an internal app available in the App Launcher, you also need a CustomApplication metadata type that references the UIBundle. See [Internal and External App Files](#internal-and-external-app-files). Here's an example of the UI bundle metadata that's deployed with an internal user app. ```xml myapp A Salesforce UI Bundle. true 1 CustomApplication ``` The `isActive` flag controls whether the deployed bundle is served, and `version` is a schema-version integer that starts at 1. ## The ui-bundle.json File The `ui-bundle.json` file, located in the `force-app/main/default/uiBundles/{app_name}` folder, is the app's runtime configuration file. The file can include these properties. | Property | Type | Description | | ------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `outputDir` | string | Required. The relative path to the directory that contains built application assets. The default value is "`dist`" for bundled apps. | | `apiVersion` | string | The Salesforce API version. Uses the format "`vXX.X`"; for example, "`v66.0`". Be sure to use the same API version as the Salesforce org where you're deploying your app. Defaults to the current org version. | | `routing` | string | Block containing custom URL handling. If omitted, standard file-based serving is used. | | `routing.fileBasedRouting` | boolean | When `true`, URLs are automatically mapped to the folder structure in `outputDir`. Defaults to `true`. | | `routing.trailingSlash` | string | Controls how the platform handles trailing slashes in URLs. Valid values are:
• `never`: Removes the slash (for example, redirects "/page/" to "/page").
• `always`: Adds a slash (for example, redirects "/page" to "/page/").
• `auto`: No change. | | `routing.fallback` | string | Specifies the file to serve when a request doesn't match any static asset or explicit rule. Set to `index.html` to allow client-side routing for single-page apps (SPAs). | | `routing.rewrites` | array | An array of rewrite rules that serve a different file without changing the browser URL. | | `routing.rewrites.route` | string | Required for `rewrites`. The source URL pattern. Supports named parameters (`:id`) and wildcards (`*`). | | `routing.rewrites.target` | string | Required for `rewrites`. The internal file path to serve. | | `routing.redirects` | array | An array of redirect rules that return an HTTP redirect response to the client. Rules are evaluated in order. | | `routing.redirects.route` | string | Required for `redirects`. The source URL pattern. Supports named parameters (`:id`) and wildcards (`*`). | | `routing.redirects.target` | string | Required for `redirects`. The destination URL. | | `routing.redirects.statusCode` | string | Required for `redirects`. The HTTP status for the redirect:
• **301**: Moved permanently (cacheable)
• **302**: Found (temporary, not cached)
• **307**: Temporary redirect (preserves request method, not cached)
• **308**: Permanent redirect (preserves request method, cacheable) | This example shows all possible configuration options for a `ui-bundle.json` file. ```json { "outputDir": "dist", "apiVersion": "v66.0", "routing": { "fileBasedRouting": true, "trailingSlash": "never", "fallback": "index.html", "rewrites": [ { "route": "/products/:id", "target": "/product-detail.html" }, { "route": "/legacy/*", "target": "/app.html" } ], "redirects": [ { "route": "/old-home", "target": "/", "statusCode": 301 }, { "route": "/promo", "target": "/sale", "statusCode": 302 } ] } } ``` This example shows a typical configuration file for a single-page app, which ensures that the app works correctly when a user refreshes the page on a sub-route (for example, `/dashboard`). ```json { "outputDir": "dist", "routing": { "trailingSlash": "never", "fallback": "index.html" } } ``` ## Internal and External App Files When you create an app, you choose an internal or an external template, which provides the necessary UIBundle project structure along with the related metadata that differs by use case. The template names differ by framework—for example, `reactinternalapp` and `reactexternalapp` for React, or `angularinternalapp` and `angularexternalapp` for Angular. For the full list of templates and the generate commands, see [Generate an App from a Template](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-generate-app.md). ### Internal App Files Use an internal template to create internal-facing apps for employees signing in with their Salesforce credentials. Projects based on an internal template also require the CustomApplication metadata. This type defines and configures a Salesforce app, and connects your app to the App Launcher. The `force-app/main/default/applications` folder contains the `.app-meta.xml` file, which includes a field called `uiBundle`. This field contains the fully qualified name of the UIBundle that represents your app, such as `c__myapp`. When a CustomApplication references the UIBundle through its `uiBundle` field, and the UIBundle's `target` is set to `CustomApplication` in [the `.uibundle-meta.xml` file](#the-uibundle-metaxml-file), the CustomApplication appears in the App Launcher and renders its UI from the UIBundle. ```shell my-sfdx-project/ └── force-app/main/default/ ├── uiBundles/ │ └── myinternalapp/ │ ├── myinternalapp.uibundle-meta.xml # target: CustomApplication │ ├── ui-bundle.json │ └── src/ └── applications/ └── MyInternalApp.app-meta.xml # CustomApplication metadata ``` **note:** Similar to a standard Salesforce app, you can control user access to the app using profiles and permission sets. The internal templates also generate an ` Access` permission set that grants app visibility and the API Enabled permission. For more information, see [Manage Your App and Grant User Access](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-manage.md). ### External App Files Use an external template to create external-facing apps for business-to-business (B2B) and business-to-consumer (B2C) use cases, such as partners or customers signing in from outside your org. The template provides a prebuilt shell that includes an Experience Cloud site with several pages, navigation, and authentication. Projects based on an external template also require several additional metadata types to support the Experience Cloud site. The files are contained in these folders in `force-app/main/default`: - `digitalExperienceConfigs`: The DigitalExperienceConfig metadata type represents the configuration details for your org's workspaces, such as the site label, site URL path prefix, and workspace type. - `digitalExperiences`: The DigitalExperience metadata type represents a collection of related content items that form the site when combined with data from the DigitalExperienceConfig metadata type. - `networks`: The Network metadata type represents an Experience Cloud site. - `sites`: The CustomSite metadata type represents a Salesforce site. Unlike a standard LWR site, the `digitalExperiences` folder contains only the `sfdc_cms_site` folder. In the `content.json` file in the folder: - The `appContainer` property is set to `true` to indicate that the site is used in a Multi-Framework app. - The `appSpace` property refers to the related app. Make sure that the value uses the format `NamespacePrefix__DeveloperName` (for example, `MyNamespace__MyWebApp` or `c__MyWebApp` for orgs without a registered namespace). ```shell my-sfdx-project/ └── force-app/main/default/ ├── uiBundles/ │ └── myexternalapp/ │ ├── myexternalapp.uibundle-meta.xml # target: Experience │ ├── ui-bundle.json │ └── src/ ├── digitalExperienceConfigs/ # Site configuration ├── digitalExperiences/ # Site content ├── networks/ # Experience Cloud site └── sites/ # CustomSite metadata ``` **note:** You can't edit a Multi-Framework app's Experience Cloud site in Experience Builder. ## See Also - [Generate an App from a Template](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-generate-app.md) - [Manage Your App and Grant User Access](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-manage.md) - _Metadata API Developer Guide_: [UIBundle](https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_uibundle.htm) - [Salesforce DX Project Structure and Source Format](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_source_file_format.htm) # Quick Start: Build and Run Your First App Scaffold a Salesforce Multi-Framework app, run it locally against your org, and deploy it in about 10 minutes. This quick start uses React and the `reactinternalapp` template, which generates a complete Salesforce DX project, including the CustomApplication metadata that makes your app appear in the App Launcher. For the concepts behind each step, and for options this quick start doesn't cover (such as external apps or Agentforce Vibes), see [Salesforce Multi-Framework Overview](https://developer.salesforce.com/docs/platform/multiframework/guide/multiframework-overview.md). ## Before You Begin Before starting, make sure that you: - [Set up your org for Multi-Framework app development](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-setup.md). - Install the latest [Salesforce CLI](https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_setup_update_cli.htm) and [Salesforce Extension Pack](https://marketplace.visualstudio.com/items?itemName=salesforce.salesforcedx-vscode) versions. - [Connect to a Salesforce org and set it as your default](https://developer.salesforce.com/docs/platform/sfvscode-extensions/guide/default-org.html). ## Step 1: Generate the Project Generate a new Salesforce DX project from the internal React app template. ```shell sf template generate project --name MyFirstApp --template reactinternalapp cd MyFirstApp ``` The generated project includes the UIBundle that holds your React app and the CustomApplication metadata that surfaces it in the App Launcher. ## Step 2: Install Dependencies Install the app's dependencies from inside the UIBundle directory, not the project root. ```shell cd force-app/main/default/uiBundles/MyFirstApp npm install ``` ## Step 3: Run the App Locally Start the local dev server. ```shell npm run dev ``` Open `http://localhost:5173` in your browser to see the app. To run against live org data, authenticate an org and set it as your default before you start the dev server. ## Step 4: See a Live Change With the dev server running, edit a component or page under `src/` and save. The browser hot-reloads so you can see your change immediately. In VS Code, you can also open the Command Palette and run **SFDX: Open in Live Preview** to preview your app with org context. See [Preview and Run Your App Locally](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-preview.md). ## Step 5: Deploy to Your Org When you're ready, build the production assets and deploy the project. ```shell npm run build sf project deploy start --target-org MyOrg ``` Deploying the project deploys both the UIBundle and its CustomApplication metadata. To open your app, grant your user access to it through a permission set, and then find it in the App Launcher. 1. Assign the app's permission set to your user. If the template generated one, run `sf org assign permset --name MyFirstApp_Access`. Otherwise, in Setup, use **Permission Sets** or the app's profile to grant access to the CustomApplication. 2. Open the App Launcher, search for **MyFirstApp**, and open it. For the full deployment, versioning, and publication workflow, see [Deploy and Publish a UIBundle](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-deploy.md). ## Next Steps - Access Salesforce data: [Access Data and Platform Capabilities](https://developer.salesforce.com/docs/platform/multiframework/guide/data-sdk/data-sdk-usage.md) - Match the Salesforce look and feel: [Style Your App](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-styling.md) - Add conversational AI: [Integrate Agentforce Conversation Client](https://developer.salesforce.com/docs/platform/multiframework/guide/reactdev-acc.md) - Explore working examples: [Multi-Framework recipes repo](https://github.com/trailheadapps/multiframework-recipes) # Resize an Embedded App to Fit Its Content Avoid inner scrollbars and clipped content by letting the embedding frame grow and shrink to fit your app. The embedded app reports its size to Salesforce, and Salesforce sizes the iframe to match, so your app renders as if it were a native part of the page. ## How It Works The embedded app (the guest) reports its height to Salesforce through the embedding protocol. Salesforce applies that height to the iframe. Automatic height reporting is on by default: the embedding resizer starts when your app bootstraps (when you import `@salesforce/platform-sdk/ui-embedding`) and reports the new height as your content changes. There's no SDK method to turn it on or configure it. You can also report a size yourself with the view SDK, which layers on top of the resizer, for example to set an explicit width. ## Prerequisites - Your app is embedded with `lightning-ui-embedding` and loads successfully. ## Report the Size To report your app's size directly, call `resize` from the view SDK. Pass the width and height as pixel strings, such as `"480px"`. Pass an empty string for an axis you don't want to change. ```javascript import "@salesforce/platform-sdk/ui-embedding"; import { createViewSDK } from "@salesforce/platform-sdk/view"; const viewSdk = await createViewSDK(); // Set the height and leave the width unchanged. await viewSdk.resize?.("", "480px"); ``` You don't need to enable automatic height reporting. The embedding resizer runs by default and reports the new height whenever your content changes, so it settles on the right height on its own. **important:** Avoid full-viewport height styles, such as `height: 100vh` or `min-height: 100vh`, in your embedded app. Inside the iframe, `100vh` resolves to the iframe's own current height, not the height of the full Salesforce page. So a full-viewport height can't grow to fit your content and usually just clips it. ## See Also - [Get Started with UI Embedding](https://developer.salesforce.com/docs/platform/microfrontend/guide/get-started.md) - [Style an Embedded App to Match Salesforce](https://developer.salesforce.com/docs/platform/microfrontend/guide/style-embedded-app.md) # Resize an Embedded App to Fit Its Content (Developer Preview) Let the embedding frame grow and shrink with your app's content so it renders without inner scrollbars or clipping. The embedded app reports its height, and the `` host sizes the iframe to match. **note:** This topic describes the Developer Preview API. To move an app to the generally available version, see [Migrate from the Developer Preview API](https://developer.salesforce.com/docs/platform/microfrontend/guide/migrate-from-dev-preview.md). ## How It Works Auto-resize is on by default. Inside the iframe, the bridge watches your app's content for size changes and reports the new height to the host. The host applies that height to the iframe. Resizing adjusts height only. The iframe width is always 100% of its container. ## Disable Auto-Resize To take over sizing yourself, listen for the `resize` event on the shell and cancel it. The event is cancelable. When you cancel it, the host doesn't apply the height, and sizing becomes your responsibility. ```javascript shell.addEventListener("resize", (event) => { event.preventDefault(); // apply your own height policy here }); ``` **important:** If you cancel the `resize` event, you must apply a height yourself. If you cancel it and don't set a height, the iframe never gets sized. ## Avoid Full-Viewport Heights **warning:** Avoid full-viewport height styles, such as `height: 100vh` or `min-height: 100vh`, in your embedded app. When automatic resizing is on, a full-viewport height can create a feedback loop where the content height and the frame height keep growing. More generally, don't change the observed element's size from inside a resize handler, because it re-triggers the observer. ## See Also - [Embed an External Web App (Developer Preview)](https://developer.salesforce.com/docs/platform/microfrontend/guide/dp-embed-external-web-app.md) - [Match an Embedded App to the Salesforce Theme (Developer Preview)](https://developer.salesforce.com/docs/platform/microfrontend/guide/dp-style-embedded-app.md) # Salesforce Multi-Framework Overview Use Salesforce Multi-Framework, a framework-agnostic runtime, to build Salesforce apps with frontend frameworks, such as React or Angular, and run them directly on the Salesforce Platform. Your app has access to your Salesforce data through GraphQL and the Salesforce Data SDK, along with built-in platform features, such as authentication, security, and governance. Build modern frontends backed by the trust and data of the platform. Whether you speed up development with Agentforce Vibes or use your own tooling, with Salesforce Multi-Framework you can: - Jump-start app development with ready-to-use templates for React and Angular. - Include Salesforce data and Salesforce CMS content in the app. - Add features including search, authentication, and the Agentforce Conversation Client. - Preview, deploy, and manage your app, and then access it from the App Launcher or the Salesforce mobile app. - Embed your app as a microfrontend in Lightning Experience or Experience Cloud site pages (Beta). ## How Salesforce Multi-Framework Works A few platform concepts explain how a Multi-Framework app runs on Salesforce, including the metadata type your app becomes, the kinds of apps you can build, the domain it runs on, and the security model it inherits. ### The UIBundle Metadata Type When you build a Multi-Framework app for the Salesforce Platform, your app becomes a piece of Salesforce metadata called a UIBundle. A UIBundle is a self-contained unit that holds your app's source, configuration, and routing rules. It represents your app's UI and works with several other metadata types to define your app in Salesforce. Because the UIBundle is metadata, it behaves like other Salesforce metadata. Your app is: - **Stored and versioned**: Your app's files are stored in Salesforce, and each UIBundle has a version. You manage the publication lifecycle on the platform rather than on an external web server. - **Managed with Salesforce DX**: An app is part of a Salesforce DX project. You use familiar Salesforce DX workflows, CLI commands, and project structures to manage the app alongside your other metadata, such as custom objects and Apex classes. - **Deployed as metadata**: You deploy a UIBundle to an org the same way you deploy any other source, and you can include it in your deployment pipeline. Your app’s files live in the Salesforce DX project under the `force-app/main/default/uiBundles/` directory. ### Internal Vs External Apps With Salesforce Multi-Framework, you can build two kinds of apps based on the audience. - **Internal apps** are for employees (B2E) who sign in with their Salesforce credentials. After you deploy an internal app, users open it from the Salesforce App Launcher on desktop or in the Salesforce mobile app. Permission sets or user profiles control app access, just like with Lightning apps. - **External apps** are for people outside your Salesforce org, such as customers (B2C) and partners (B2B). External apps are surfaced through an Experience Cloud site, so visitors don't need to be logged in to Salesforce. The app also appears in the App Launcher for Salesforce users, where selecting it opens the site's URL in a new tab. An internal app and an external app use different project templates and platform configuration, and require some additional metadata types to integrate with the Salesforce Platform. Each audience has its own set of templates, which determine the metadata and prebuilt features your app starts with. For the full template and metadata comparison, see [Generate an App from a Template](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-generate-app.md). ### The Salesforce App Domain Internal Multi-Framework apps run on the Salesforce app domain, a separate, top-level domain that hosts web apps on Salesforce and isolates them with strong, browser-native boundaries. Because each app runs on its own origin under `*.salesforce.app`, the browser's same-origin policy separates it from the core Salesforce runtime, so your app and its third-party dependencies can't directly access the Lightning DOM, browser storage, or Salesforce session cookies. That isolation limits the risk from vulnerable third-party code. Users still get seamless single sign-on through a scoped session that lets your app securely access supported platform APIs. ### Built-In Platform Security and Governance Your app's data requests flow through the User Interface API, which enforces the same security and governance rules that apply everywhere else in Salesforce: - Object-level security (OLS) controls which objects a user can access. - Field-level security (FLS) controls which fields a user can view and edit. - Sharing rules control which records a user can access. - Validation rules run when a user creates or updates a record. Because these rules are enforced on the server for every query and mutation, you don't reimplement access control in your frontend code. Your app inherits Salesforce trust instead of managing it externally. ## Multi-Framework Vs LWC LWC remains the optimal framework for building integrated, reusable components within the Salesforce Platform. While Multi-Framework broadens developer choice, they serve different purposes: - **LWC:** Choose LWC for native Salesforce platform integration. With LWC, you get built-in security, data binding, and the ability to create highly reusable, composable components that function seamlessly within Lightning Experience, Experience Builder sites, and Salesforce mobile apps. - **Multi-Framework:** Choose React or Angular for self-contained single-page applications (SPAs) or highly customized experiences using Salesforce as the host and data source. | Feature | Lightning Web Components (LWC) | Multi-Framework | | :------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Salesforce Integration** | Deep, native integration with the Salesforce Platform. Automatic data binding, security, and access to platform services such as Lightning Data Service and Apex. | Excellent for building full page applications or custom experiences hosted via Salesforce Multi-Framework, giving developers the choice to leverage their existing framework expertise and libraries. | | **Performance** | Optimized for the Salesforce Platform, often leading to better performance within the Salesforce UI. | High performance for complex UIs, but performance within the Salesforce ecosystem depends heavily on API design and data retrieval strategies. | | **Security & Governance** | Enforces platform security and compliance standards automatically. Built-in mechanisms for managing state and platform interaction. | Data requests flow through the User Interface API, which enforces the same object-, field-, and record-level security as the rest of Salesforce. | | **Learning Curve** | Relatively low for developers familiar with modern JavaScript and HTML; syntax is standard-based. | Low to moderate, depending on familiarity with their framework of choice | | **Component Reuse** | Excellent component-level reuse, referential integrity, and seamless integration within the Lightning Experience and across LWC projects. | Component reuse is fundamental within a web app or static site. Integration of those components in Lightning Experience and other LWC-based environments requires Micro-Frontend support. | | ## Choose Your Development Approach Salesforce Multi-Framework gives you flexible options for building apps or reusing your existing frontend assets on the Salesforce Platform. With Agentforce Vibes, you can accelerate the entire development lifecycle. Because Agentforce Vibes is connected to your Salesforce org, it understands your org's metadata, objects, and Apex dependencies. And because it operates entirely within the Salesforce Trust Boundary, your schema, business logic, and source code never leave your Salesforce environment to reach an external model. You describe what you want to build in natural language, and Agentforce Vibes handles scaffolding, metadata, and business logic. Use a template to get started, and add Salesforce data, Salesforce CMS content, and features such as search. Alternatively, for low-code users, you can use Builder Central (Beta) to create apps using natural language prompts in an intuitive user interface that's available in Setup. If you prefer to use your own tooling or you want to reuse existing apps, you can manually integrate your apps with the Salesforce Platform. Because a Multi-Framework app is a standard Salesforce DX project with an npm-based build, you can develop it with whatever tools you already use, including agentic coding tools such as Claude Code and Cursor. Familiar Salesforce DX workflows, CLI commands, and project structures let you manage your app alongside your other Salesforce metadata. ## Add Data, Content, and Features After you scaffold an app, you connect it to Salesforce and extend it with platform features: - **Salesforce data**: Query and mutate records with the Data SDK (`@salesforce/platform-sdk/data`), which is framework-agnostic and sends GraphQL operations through the User Interface API. - **Salesforce CMS content**: Bring images, branding, and other managed content into your app. - **Object search**: Add search over your Salesforce records. - **Authentication**: Use Salesforce credentials for internal apps, or Experience Cloud guest and authenticated access for external apps. - **Agentforce Conversation Client**: Embed conversational AI interactions in your app. ## Embed Your App in Other Salesforce Experiences (Beta) Beyond running as a standalone app, you can use Salesforce Microfrontends (UI Embedding) to embed your deployed internal app as a microfrontend inside a Lightning page or an Experience Cloud site. Salesforce hosts your app in a secure, sandboxed iframe and gives it a channel to exchange data, styling, and lifecycle events with the surrounding page. ## See Also - _React Documentation_: [Quick Start](https://react.dev/learn) - _Agentforce Vibes Developer Guide_: [Build a React App with Agentforce Vibes](https://developer.salesforce.com/docs/platform/agentforcevibes/guide/afv-reactdev-overview.html) - _Salesforce Help_: [Get Started with Builder Central (Beta)](https://help.salesforce.com/s/articleView?id=platform.builder_central_get_started.htm&type=5) - [Multi-Framework recipes repo](https://github.com/trailheadapps/multiframework-recipes) - _Salesforce Developers Blog_: [Build with React, Run on Salesforce: Introducing Salesforce Multi-Framework](https://developer.salesforce.com/blogs/2026/04/build-with-react-run-on-salesforce-introducing-salesforce-multi-framework) # Set Up Your Org for Multi-Framework App Development Before you begin working with Multi-Framework apps, configure the Salesforce app domain and review the prerequisites for your org. If you want to add an Agentforce Conversation Client, complete the additional configuration steps. If you're using Agentforce Vibes to build your app and you want to use content from Salesforce CMS, complete the steps to set up the Content Read-Only MCP Server. ## Review Prerequisites and Considerations **Available in**: Salesforce Multi-Framework is available in Enterprise, Performance, Unlimited, Developer, and Partner Developer editions hosted on Hyperforce. Unavailable in Alibaba Cloud or Salesforce Government Cloud. Before you begin: - Confirm that your org is hosted on Hyperforce. Salesforce Multi-Framework is available only on Hyperforce. 1. From Setup, in the Quick Find box, enter `Company Information`, and then select **Company Information**. 2. In the Organization Detail section, if the instance name has a three-letter prefix followed by a number, such as abc123, your org is on Hyperforce. - Confirm that you have the latest versions of [Salesforce CLI](https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_setup_update_cli.htm) and the [Salesforce Extension Pack](https://marketplace.visualstudio.com/items?itemName=salesforce.salesforcedx-vscode) installed. This step ensures that you have the latest Agentforce Vibes and CLI capabilities, such as updated Salesforce skills and Live Preview. - To create an external Multi-Framework app, such as a public-facing portal for customers or partners, make sure that: - Digital Experiences is enabled in your org. See [Enable Digital Experiences](https://help.salesforce.com/s/articleView?id=experience.networks_enable.htm&type=5) in Salesforce Help. - You have the correct user licenses. You need Customer Community or Customer Community Plus user licenses for B2C portal users, and Partner Community and Channel Account for B2B users. Keep these limitations and considerations in mind: - Packaging is unsupported for external-facing apps. - For developer orgs, only `c` namespaces are supported. ## Configure the Salesforce App Domain | USER PERMISSIONS NEEDED | | | ------------------------------------ | ------------------------- | | To enable the Salesforce App Domain: | **Customize Application** | To create an internal Multi-Framework app for employees who log in with their Salesforce credentials, confirm that the Salesforce app domain is enabled. This separate, top-level domain hosts web apps on Salesforce while enforcing strong browser-native isolation boundaries. The domain is automatically enabled for most orgs and serves internal apps from `.my.salesforce.app`. For the Salesforce app domain to function correctly, Salesforce Edge Network is also required. See [Salesforce Edge Network](https://help.salesforce.com/s/articleView?id=xcloud.domain_name_edge_network.htm&type=5) in Salesforce Help. 1. From Setup, in the Quick Find box, enter `Salesforce Multi-Framework`, and then select **Salesforce Multi-Framework Apps**. 2. If you see the **Enable Domain** button, click it and confirm your change. If not, the domain is already enabled. 3. In the Quick Find box, enter `My Domain`, and then select **My Domain**. 4. In the Routing and Policies section, if Salesforce Edge Network isn’t enabled, click **Edit**, enable the setting, and save your changes. ## (Optional) Set Up the Content Read-Only MCP Server and Tools When you vibe code a Multi-Framework app, Agentforce Vibes can use the Content Read-Only MCP server and the Content Write MCP server to access, manage, and apply Salesforce CMS resources, such as content, images, or brands to your app. - Use the Content Read-Only server to search for, retrieve, and apply images, documents, or other content types to your Multi-Framework app. - Use the Content Write MCP server to edit existing CMS content, create content, and publish content, so that you can flexibly create CMS content for your app as you build and save it to your Salesforce org. See the Salesforce Hosted MCP Servers developer guide for information about how to set up the [Content Read-Only](https://developer.salesforce.com/docs/platform/hosted-mcp-servers/guide/content-readonly.html) and [Content Write MCP](https://developer.salesforce.com/docs/platform/hosted-mcp-servers/guide/content-write.html) servers and for more details on the available tools within each server. ## (Optional) Configure Agentforce Conversation Client With the Agentforce Conversation Client (ACC), you can integrate conversational AI interactions into your Multi-Framework apps for internal employee use cases. For more details on Agentforce Conversation Client, check out the [Agentforce Conversation Client (ACC) Developer Guide](https://developer.salesforce.com/docs/platform/accsdk/overview). Your Salesforce org must meet specific architectural requirements to support session persistence between Salesforce and the Multi-Framework app. ### Set Up Agentforce Employee Agent You must have at least one Agentforce Employee Agent (AEA) configured with topics and actions. Additionally, ensure that the global Agentforce preference is enabled: 1. From Setup, in the Quick Find box, enter `Agentforce Agents` and then select **Agentforce Agents**. 2. Enable the **Agentforce** preference. ### Configure Routing and Policies To ensure that the ACC can maintain state within the Multi-Framework app, the Salesforce org must permit the use of cookies for the integration path. **Cookie Policy:** The requirement for first-party use of Salesforce cookies must be disabled within the My Domain settings. 1. From Setup, in the Quick Find box, enter `My Domain`, and then select **My Domain**. 2. In the Routing and Policies section, if **Require first-party use of Salesforce cookies** is enabled, click **Edit**, and then deselect the setting. 3. Save your changes. ### Set Up Trusted Domains for Agentforce Conversation Client Because Multi-Framework apps are typically hosted on origins external to Salesforce (including localhost during development), the Salesforce iframe must be explicitly authorized to render on those domains. **Domain Authorization:** The application's hosting domain (for example, `https://myapp.com` or `localhost:`) must be registered in the Trusted Domains for Inline Frames registry within Salesforce Session Settings. 1. From Setup, in the Quick Find box, enter `Session`, and then select **Session Settings**. 2. In the Trusted Domains for Inline Frames section, click **Add Domain**. 3. Enter the domain for your web app, such as `http://localhost:`. For example, `http://localhost:5173`. 4. Select **Lightning Out** for the iFrame type and save your changes. # Style an Embedded App to Match Salesforce Keep an embedded app visually consistent with the surrounding Salesforce experience. Salesforce passes styling values to the embedded app as CSS custom properties, so your app can read them and match the page's colors, spacing, and other theme values. ## How It Works Salesforce composes a UI state for each embedded app and pushes it to the guest through the embedding protocol. That UI state includes a `styles` object with two parts: a `variables` object built from CSS custom properties (styling tokens) and an `attributes` object of allowlisted HTML attributes. Your embedded app reads these values from the current state and applies them to its own styles. When the values change, Salesforce pushes an updated UI state, and your app restyles by subscribing to those updates. The host sets both parts on the `lightning-ui-embedding` element itself. Inline CSS custom properties (`style="--x: y"`) become `styles.variables`, and the allowlisted HTML attributes become `styles.attributes`. The component reads them off the element and forwards them to your embedded app. **note:** Theming is scoped to CSS custom properties. Salesforce doesn't automatically restyle your app's components or replicate the full design system inside your app. Your app decides how to apply the token values. ## Prerequisites - Your app is embedded with `lightning-ui-embedding` and loads successfully. ## Read the Styling Tokens 1. In your embedded app, create the view SDK and read the current UI state. `createViewSDK` is asynchronous, so await it. `getUiState` returns the current state and a `subscribe` function you use to react to later updates. ```javascript import "@salesforce/platform-sdk/ui-embedding"; import { createViewSDK } from "@salesforce/platform-sdk/view"; const viewSdk = await createViewSDK(); // Off an embedding surface, createViewSDK resolves to an empty object, // so guard the call before you use it. const uiState = viewSdk.getUiState?.(); if (uiState) { applyStyles(uiState.state.styles?.variables); } ``` 2. Apply the CSS custom properties from `state.styles.variables` to your app. The variables arrive as name-value pairs. Map them to your app's styles so your colors and spacing follow the Salesforce theme. 3. Apply the allowlisted HTML attributes from `state.styles.attributes` to your app. Along with the variables, `styles.attributes` mirrors these attributes so your app can match the host: `autocapitalize`, `autocorrect`, `dir`, `enterkeyhint`, `inputmode`, `lang`, `spellcheck`, `title`, and `translate`. Applying `dir` (RTL or LTR) and `lang` is especially important for matching the Salesforce experience. ```javascript const { variables, attributes } = uiState.state.styles ?? {}; applyStyles(variables); applyAttributes(attributes); ``` 4. Restyle when the theme changes. Salesforce pushes an updated UI state when styling values change, so subscribe and reapply. ```javascript const unsubscribe = uiState.subscribe((next) => { applyStyles(next.styles?.variables); applyAttributes(next.styles?.attributes); }); ``` **note:** The embedded app resolves its own platform context, such as locale and form factor. Salesforce doesn't send `theme`, `locale`, or `formFactor` as part of the UI state. ## See Also - [Get Started with UI Embedding](https://developer.salesforce.com/docs/platform/microfrontend/guide/get-started.md) - [Resize an Embedded App to Fit Its Content](https://developer.salesforce.com/docs/platform/microfrontend/guide/resize-embedded-app.md) # Style Your Apps React apps don't currently support Lightning base components and `lightning/*` modules, so they don't provide built-in Salesforce Lightning Design System (SLDS) styling. With React apps, there are several ways to match the Salesforce look-and-feel. - Apply SLDS blueprints - Import SLDS for React from @salesforce/design-system-react Alternatively, use [shadcn](https://ui.shadcn.com/) to create building blocks that you can customize via Tailwind CSS based on your requirements. **tip:** The styling examples in the [Multi-framework recipes repo](https://github.com/trailheadapps/multiframework-recipes) demonstrate how to use SLDS blueprints, SLDS for React, and shadcn components. ## Apply SLDS Blueprints To create UI components that match the Salesforce look-and-feel, use the SLDS blueprint for the component. For example, if you're creating a button, apply the [SLDS button blueprint](https://v1.lightningdesignsystem.com/components/buttons/) in your app. Unlike [Lightning base components](https://developer.salesforce.com/docs/platform/lightning-component-reference/guide/), you control the markup for your UI components but don't get the latest SLDS updates automatically. ```typescript
``` ## Import SLDS for React To apply SLDS styling to your UI components automatically, import SLDS for React in your app. ```typescript import { Button } from "@salesforce/design-system-react"; export default function ButtonDSR() { return (

Disabled

); } ``` SLDS for React comes with icons that you get via the `lightning-icon` LWC component. With shadcn, wrap your icon using `IconSettings` to configure the sprite base path. ```typescript import { Icon, IconSettings } from "@salesforce/design-system-react"; export default function MyReactIcon() { return ( ); } ``` ## Use shadcn Components In LWC, styles are implemented in the base components and you apply SLDS design tokens to customize the styling. In a React app with shadcn, styling is applied globally. When you install a shadcn component, the source code is added to your project. You can then change the Tailwind classes directly in that file to alter the branding globally. For example, you can install the shadcn Button and Card components like this. ```javascript import React from "react"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; ``` A React app uses a global CSS file to define styles that apply across the entire app. Use a `global.css` file as your design system entry point where you can define tokens and map them into Tailwind utilities. With this pattern, you can swap token values to rebrand without rewriting components. ### Import Tailwind and any shared styles To create your global file, start with the Tailwind import and apply base defaults using `@layer`. Use `@layer base` to set app-wide defaults, such as for `min-h-screen, antialiasing, default borders, focus outlines, base bg/text`. ```css /* global.css */ @import "tailwindcss"; @layer base { html, body, #root { @apply min-h-screen; } body { @apply antialiased bg-white; } } @import "tw-animate-css"; @import "shadcn/tailwind.css"; ``` ### Define semantic design tokens in :root Brand your app by overriding tokens. The global CSS file is where you can define the app-level tokens. For example, you can add a `.dark` block for dark-mode token overrides, which cascades automatically. ```css :root { --radius: 0.625rem; --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); --primary: oklch(0.205 0 0); --border: oklch(0.922 0 0); } .dark { --background: oklch(0.145 0 0); --foreground: oklch(0.985 0 0); } ``` ### Map tokens into Tailwind theme variables Use `@theme` inline to map semantic tokens to utility-facing names, such as `--color-primary` and `--color-background`. This lets you write classes like `bg-background`, `text-foreground`, `border-border` in components. For more information, see the [shadcn theming documentation](https://ui.shadcn.com/docs/theming). ```css @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); } ``` **tip:** The `global.css` example in the [Multi-framework recipes repo](https://github.com/trailheadapps/multiframework-recipes) demonstrates how to configure Tailwind CSS with custom theme variables by using CSS custom properties. ## Example Conversion Here's an example of a `lightning-card` component for LWC that we want to convert to a shadcn Card component. ```html ``` Converting a `lightning-card` to a shadcn `Card` is a move from a **template-driven** approach to a **composition-driven** approach. In LWC, the structure is defined by slots (title, actions, footer). In React with shadcn, you explicitly render every piece of the card by using sub-components: `Card, CardHeader, CardTitle, CardDescription, CardContent,` and `CardFooter`. ```typescript // MyComponent.tsx import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Building2 } from "lucide-react"; // Equivalent to standard:account export default function AccountCard() { return (
Account Details Customer Tier: Gold
{/* Replacing slot="actions" */}

Account Name: Acme Corp

Industry: Technology

Last Updated: March 2026

); } ``` Consider several styling differences between SLDS and shadcn components. - **Radius:** SLDS 1 uses a small border-radius (4px/0.25rem) for buttons and cards, while SLDS 2 uses a border-radius of 15rem. The default shadcn radius is 0.5rem. - **The "ring" focus indicator color:** With SLDS, focus states are often the brand blue or similar to `#0176d3` so your shadcn inputs feel native to the Lightning experience. - **Secondary colors:** SLDS uses light grays (`#f3f3f3`) for secondary backgrounds. shadcn's default `secondary` is often a darker gray. For more information, see the [SLDS Color Overview documentation](https://v1.lightningdesignsystem.com/guidelines/color/overview/). ## Utility Classes To style your components, use [Tailwind CSS utility classes](https://tailwindcss.com/docs/styling-with-utility-classes). For example, you can use the [flexbox and grid](https://tailwindcss.com/docs/align-items) utility classes instead of the SLDS utility classes. ```html
🟡

{application.applicantName} - {application.propertyAddress}

Submitted: {application.submittedDate}

``` # Test Your App A Multi-Framework app is a standard React or Angular project, so you test it with your framework's usual tools. Each project template comes preconfigured for both unit tests and end-to-end tests, so you can verify your app locally and in your pipeline before you deploy it to an org. This topic applies to both React and Angular apps. Run the commands from your UIBundle directory (`/uiBundles//`), where your app's `package.json` and test configuration live. ## Run Unit Tests Each template includes a `test` script. From your UIBundle directory, run: ```bash npm run test ``` - **React** apps use [Vitest](https://vitest.dev/) with [Testing Library](https://testing-library.com/docs/react-testing-library/intro/) and a jsdom environment. The template configures this in `vitest.config.ts` and `vitest.setup.ts`. Put a test file next to the component it covers, such as `MyComponent.test.tsx`. `npm run test` starts Vitest in watch mode and reruns tests as you edit. For a single pass, such as in CI, run `npm run test -- run`. - **Angular** apps use `ng test`, which runs Angular's built-in unit test runner against your `*.spec.ts` files. ## Run End-to-End Tests Each template includes [Playwright](https://playwright.dev/) for browser-based end-to-end tests, with a `playwright.config.ts` file and an `e2e` directory that holds a sample `app.spec.ts`. The Playwright configuration starts a local static server that serves your compiled app and runs the tests against it, so the tests don't need a connection to a Salesforce org. Build your app for end-to-end testing first. This command builds your production assets and rewrites their paths so that Playwright can serve them: ```bash npm run build:e2e ``` Then run the tests: - **React**: `npx playwright test` - **Angular**: `npm run e2e` ## Test Before You Deploy Run your unit and end-to-end tests as part of your build, before you deploy the UIBundle to an org. Because the app is source-format metadata in your Salesforce DX project, you can run these tests as a step in the same CI/CD pipeline that deploys your app. See [Deploy and Publish a UIBundle](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-deploy.md). ## See Also - [Preview and Run Your App Locally](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-preview.md) - [Deploy and Publish a UIBundle](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-deploy.md) # Test, Deploy, and Distribute Your App After you build a Multi-Framework app, you take it from a local project to a published app that users can access. You test your app, deploy the UIBundle to your org, grant users access, and optionally embed or package it for distribution. Because your app is Salesforce metadata, each of these steps uses the same Salesforce DX workflows that you use for other source. ## Move Your App to Production Work through these tasks in order: 1. **Test your app.** Run the unit and end-to-end tests that ship with your template before you deploy. See [Test Your App](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-testing.md). 2. **Deploy and publish a UIBundle.** Deploy your app to your org as metadata and publish a version so that users can access it. See [Deploy and Publish a UIBundle](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-deploy.md). 3. **Manage your app and grant access.** Manage the deployed app in Setup and grant users access. See [Manage Your App and Grant User Access](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-manage.md). ## Distribute Your App Share your app beyond a single org: - [Embed Your App in Salesforce with UI Embedding](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-ui-embedding.md) - [Package and Distribute Your App](https://developer.salesforce.com/docs/platform/multiframework/guide/mfw-packaging.md) # Track Unsaved Changes in an Embedded App When a user edits data inside an embedded app and then tries to navigate away, Salesforce shows its standard unsaved-changes warning. It's the same prompt users see on native Salesforce pages. Your embedded app tells Salesforce when it has unsaved work, and the platform handles the confirmation prompt. ## How It Works From inside the embedded app (the guest), you use the view SDK to mark the view as dirty when the user makes an unsaved change, and to clear that state when the user saves. Salesforce re-bubbles this state to the platform navigation guard, which shows the standard confirmation when the user navigates away. Your app doesn't manage the browser's `beforeunload` event. The platform does that. ## Prerequisites - Your app is embedded with `lightning-ui-embedding` and loads successfully. - Your app runs in Lightning Experience or an LWR-based Experience Cloud site. These surfaces observe the dirty-state signal natively. **note:** Dirty-state tracking isn't supported in the Salesforce mobile app. ## Mark and Clear the Dirty State 1. In your embedded app, import and create the view SDK. ```javascript import "@salesforce/platform-sdk/ui-embedding"; import { createViewSDK } from "@salesforce/platform-sdk/view"; const viewSdk = await createViewSDK(); ``` 2. When the user makes an unsaved change, mark the view as dirty. ```javascript await viewSdk.markDirtyState?.("profileForm"); ``` 3. When the user saves, clear the dirty state. ```javascript await viewSdk.clearDirtyState?.("profileForm"); ``` The `label` argument identifies a specific region of unsaved work, so you can track more than one independently. **note:** On an embedding surface, `markDirtyState` and `clearDirtyState` are always available. Off an embedding surface, `createViewSDK` resolves to an empty object and these methods are `undefined`. Guard every call with optional chaining, as shown with `viewSdk.markDirtyState?.(...)`, so your app works predictably across surfaces. ## Known Limitation The unsaved-changes confirmation doesn't appear when a user closes an embedded app that's inside a modal by clicking the modal's close (X) button. Dismissing a modal isn't a navigation, so the platform navigation guard doesn't run. If your app runs inside a modal, implement your own confirm-on-close. ## See Also - [Get Started with UI Embedding](https://developer.salesforce.com/docs/platform/microfrontend/guide/get-started.md) # UI Embedding Error Codes When you embed a web app with the UI Embedding host component, the host reports failures by dispatching a single DOM event, `sf-embedding.component.error`, on its own element. Listen for that event to detect and handle every error the host surfaces to your page. Each error event carries this detail shape: ```json { "instanceId": "string", "phase": "configuration | bootstrap", "code": "STRING_CODE", "message": "Human-readable description of the failure.", "retryable": false } ``` - `phase` groups the failure by the stage at which it occurred: `configuration` (a setup or attribute problem) or `bootstrap` (the handshake with the embedded document). - `code` is a stable string identifier for the specific error. Match on `code` in your handler. - `retryable` indicates whether a fresh attempt (for example, remounting the component) is plausibly worthwhile. When `retryable` is `false`, fix the underlying problem first. **note:** The `sf-embedding.component.error` event is the only channel through which the host surfaces errors to your page. Errors that the host records for internal diagnostics only aren't delivered through this event and aren't listed here. ## Error Codes Match on the `code` value in your `sf-embedding.component.error` handler. | Code | Phase | Retryable | | --------------------------- | ------------- | --------- | | `ALLOW_SCRIPTS_STRIPPED` | configuration | No | | `BLOCKED_SANDBOX_TOKEN` | configuration | No | | `MISSING_SRC` | configuration | No | | `INVALID_SRC` | configuration | No | | `SAME_ORIGIN_SRC` | configuration | No | | `SESSION_BINDING_MUTATED` | configuration | No | | `WRONG_ORIGIN` | bootstrap | No | | `PROTOCOL_VERSION_MISMATCH` | bootstrap | No | | `INSTANCE_ID_MISMATCH` | bootstrap | No | | `DUPLICATE` | bootstrap | No | | `WRONG_TYPE` | bootstrap | No | | `DUPLICATE_PORT_TRANSFER` | configuration | No | | `HEARTBEAT_TIMEOUT` | bootstrap | Yes | | `TRANSFER_FAILED` | bootstrap | No | | `EMBEDDING_SHUTDOWN` | configuration | No | Most of these codes report a setup or attribute problem that you fix in your own configuration. A few of them, such as `HEARTBEAT_TIMEOUT`, `TRANSFER_FAILED`, and `EMBEDDING_SHUTDOWN`, report a runtime or lifecycle condition in the embedded app or the frame rather than a mistake in your setup. They're documented here so you can recognize and handle them, but they don't always call for a configuration change. ## Configuration Errors These errors occur before the handshake, when the host validates the attributes you set on the component. ### ALLOW_SCRIPTS_STRIPPED **Message:** `iframe sandbox MUST include 'allow-scripts'; embedding cannot run without it.` The resolved `sandbox` attribute for the embedding frame doesn't include the `allow-scripts` token. The embedded app can't run its scripts without it, so the embedding can't start. To resolve: 1. Review the `sandbox` and `removeSandboxTokens` values you set on the component. 2. Make sure that the resolved sandbox keeps `allow-scripts`. 3. Remove `allow-scripts` from `removeSandboxTokens` if you added it there. ### BLOCKED_SANDBOX_TOKEN **Message:** `iframe sandbox MUST NOT include 'allow-top-navigation'; poses phishing risk.` or `iframe sandbox MUST NOT include 'allow-popups-to-escape-sandbox'; poses security risk.` The resolved sandbox includes a token that UI Embedding blocks for security reasons: `allow-top-navigation` or `allow-popups-to-escape-sandbox`. To resolve: 1. Remove `allow-top-navigation` and `allow-popups-to-escape-sandbox` from the `sandbox` value. 2. Reload the component. ### MISSING_SRC **Message:** `iframe src MUST be provided; the embedding cannot start without a src.` No `src` value was set on the component. The embedding can't start without a source URL. To resolve: 1. Set the `src` attribute to the URL of the app that you want to embed. ### INVALID_SRC **Message:** `iframe src MUST be a parseable URL.` On the `src`-mutation path, the same code carries an appended detail: `iframe src MUST be a parseable URL. Error: {details}`. The `src` value isn't a URL that the host can parse. To resolve: 1. Set `src` to a complete, absolute URL, such as `https://app.example.com/widget`. 2. Confirm that the value has no leading spaces or unresolved template placeholders. ### SAME_ORIGIN_SRC **Message:** `iframe src origin MUST differ from the host document origin.` The `src` origin is the same as the host page's origin. UI Embedding requires the embedded app to load from a different origin. To resolve: 1. Host the embedded app on an origin that differs from the host page. 2. Update `src` to point to that origin. ### SESSION_BINDING_MUTATED **Message:** `'{property}' is session-binding; remount the component to change it.` The `src`, `sandbox`, or `removeSandboxTokens` attribute was changed after the component mounted. These attributes are read once at mount and can't change during the life of an instance. To resolve: 1. Unmount and remount the component to apply a new `src`, `sandbox`, or `removeSandboxTokens` value. ### DUPLICATE_PORT_TRANSFER **Message:** `embedding signalled shutdown.` The host detected a duplicate bootstrap envelope from the iframe bridge and shut the session down. This code is distinct from `DUPLICATE`, which reports a duplicate handshake heartbeat. `DUPLICATE_PORT_TRANSFER` arrives as the `code` on the `sf-embedding.component.error` event (see the note under EMBEDDING_SHUTDOWN). To resolve: 1. Confirm that the embedded app completes the bootstrap handshake only once per load. 2. Remount the component to start a new session. ## Bootstrap Errors These errors occur during the handshake between the host and the embedded document. ### WRONG_ORIGIN **Message:** `heartbeat origin '{origin}' does not match the expected iframe src origin '{expected}'` The handshake message arrived from an origin that doesn't match the origin of the `src` URL. To resolve: 1. Confirm that the embedded app sends its handshake from the same origin as its `src` URL. 2. Check for redirects that move the app to a different origin before it sends the handshake. ### PROTOCOL_VERSION_MISMATCH **Message:** `declared version '{version}' is not supported by host` The embedded app declared a protocol version that the host doesn't support. To resolve: 1. Update the embedded app to use a protocol version that the host supports. ### INSTANCE_ID_MISMATCH **Message:** `heartbeat instanceId does not match the host-minted session instanceId` The instance identifier in the handshake doesn't match the identifier the host assigned for the session. This mismatch usually means the app echoed a stale or hard-coded identifier instead of the one the host passed in. To resolve: 1. Confirm that the embedded app reads the host-provided instance identifier and returns the same value in its handshake. 2. Remove any hard-coded or cached instance identifier from the app. ### DUPLICATE **Message:** `heartbeat received after port1 was already transferred` The host received a handshake after it already completed the handshake and transferred the message port for the session. To resolve: 1. Confirm that the embedded app sends the handshake only once per load. ### WRONG_TYPE **Message:** `heartbeat rejected: WRONG_TYPE` The host received a handshake message during bootstrap, but the message wasn't a `sf-embedding/ready` heartbeat. The message was missing, wasn't an object, or carried a different type value. To resolve: 1. Confirm that the embedded app sends a well-formed `sf-embedding/ready` message as its handshake. 2. Check that no other code on the page posts messages to the host while it waits for the handshake. ### HEARTBEAT_TIMEOUT **Message:** `Embedded document did not send sf-embedding/ready heartbeat within {n}ms` The embedded document didn't send its `sf-embedding/ready` handshake within the advisory deadline. This deadline is advisory: the host reports the timeout but doesn't tear down the component, so a late handshake can still succeed. To resolve: 1. Confirm that the embedded app sends the `sf-embedding/ready` message during load. 2. Check that the app loads and runs without errors at the URL in `src`. 3. To adjust or disable the deadline, set `heartbeatTimeoutMs`. A positive number sets the deadline in milliseconds, and `Infinity` disables it. 4. Because this error is retryable, remount the component to try again after you fix the underlying delay. ### TRANSFER_FAILED **Message:** `