Admin UI SDK configuration
The adminUi field in app.commerce.config defines how your app extends the Adobe Commerce Admin UI through the commerce/backend-ui/2 extension point. It is not compatible with the commerce/backend-ui/1 extension point used by the previous version of the Admin UI SDK.
At a high level, it is the top-level config block for declaring:
- Menu entries
- Grid columns
- Mass actions
- Order view buttons
Admin UI SDK V2 handles registration automatically. Unlike V1, there is no registration action to hand-author. commerce/backend-ui/2 reads the registration directly from the generated app-config runtime action. When adminUi is defined, init and generate all automatically wire up the extension point, including the pre-app-build hook and the workerProcess declarations in ext.config.yaml. View-based features (a menu, a view mass action, or a view order view button) also get a minimal web-src/ scaffold the first time they're added, using .tsx files for TypeScript configs and .jsx files otherwise.
For general Admin UI SDK concepts and extension points outside of App Management, see the Admin UI SDK documentation.
Add a menu declaration
Declare a single Commerce Admin menu entry for the application. Use the named constants exported by @adobe/aio-commerce-lib-admin-ui/menu instead of raw strings for parentMenu:
import { defineConfig } from "@adobe/aio-commerce-lib-app/config"
import { MENU_SALES } from "@adobe/aio-commerce-lib-admin-ui/menu";
export default defineConfig({
metadata: {
// ...
},
adminUi: {
menu: {
id: "approval_dashboard",
label: "Approval Dashboard",
description: "Review and approve purchase requests from Commerce Admin.",
parentMenu: MENU_SALES,
sandboxPermissions: ["allow-popups", "allow-downloads"],
aclProtected: true,
},
},
});
id/, :, _.labeldescriptionpageTitleparentMenumetadata.displayName. When this property is omitted, Commerce places it under the Apps menu.sandboxPermissionsallow-downloads, allow-modals, allow-popups.aclProtectedtrue, Commerce generates a per-app ACL resource and adds it to the Adobe Commerce User Roles tree. See ACL-protected extension points.Add grid columns
You can add custom columns to order, product, or customer grids. Each grid's columns are fetched by a runtime action you implement; generate derives the workerProcess entry from runtimeAction automatically:
adminUi: {
order: {
gridColumns: {
label: "Order fulfillment data",
description: "Adds fulfillment status and risk score to the order grid",
runtimeAction: "orders/fetch-order-grid-data",
columns: [
{ id: "fulfillment_status", label: "Fulfillment", type: "string", align: "left" },
{ id: "risk_score", label: "Risk", type: "integer", align: "right" },
],
},
},
product: {
gridColumns: {
label: "Product inventory data",
description: "Adds inventory status to the product grid",
runtimeAction: "products/fetch-product-grid-data",
columns: [
{ id: "inventory_status", label: "Inventory", type: "string", align: "left" },
],
},
},
customer: {
gridColumns: {
label: "Customer loyalty data",
description: "Adds loyalty tier to the customer grid",
runtimeAction: "customers/fetch-customer-grid-data",
columns: [
{ id: "loyalty_tier", label: "Loyalty Tier", type: "string", align: "left" },
],
},
},
}
Common grid column properties
The order, product, and customer definitions are all optional. Configure only the grids your application extends.
labeldescriptionruntimeActionpackage/action path matching a handler you implement. Registered as a workerProcess operation automatically.Column properties
The columns array contains one or more column definitions. Each column is rendered in the grid with the specified label, type, and alignment. The id is used as the response data key in the runtime action.
idlabeltypeboolean, date, datetime, float, integer, string.alignleft, center, right.aclProtectedtrue, Commerce generates a per-app nested ACL resource for this column. See ACL-protected extension points.Grid column handler
Implement the runtimeAction handler with the typed request and response builders from @adobe/aio-commerce-lib-admin-ui/grid-columns:
import { okGridResponse, parseGridRequest } from "@adobe/aio-commerce-lib-admin-ui/grid-columns";
export async function main(params) {
const { gridType, ids } = parseGridRequest(params);
return okGridResponse({
"000000001": { fulfillment_status: "shipped", risk_score: 12 },
});
}
The parseGridRequest method throws a CommerceSdkValidationError error on a malformed request. Use errorGridResponse(status, message) to return a non-2xx response. See the @adobe/aio-commerce-lib-admin-ui usage guide for the full wire contract.
Add mass actions
Add mass actions that run against a selection of grid rows. Each entry uses type to select how it runs. When the value is view, it opens an iframe at path; when the value is worker, it invokes a runtimeAction:
adminUi: {
order: {
massActions: [
{
type: "view",
id: "export-orders",
label: "Export",
title: "Export orders",
path: "#/export-orders",
sandboxPermissions: ["allow-downloads"],
},
{
type: "worker",
id: "bulk-approve",
label: "Approve",
confirm: { message: "Approve the selected orders?" },
runtimeAction: "orders/bulk-approve",
timeout: 15,
notifications: {
success: "Orders approved.",
error: "Approval failed. Check the runtime logs.",
},
},
],
},
}
Mass actions are supported on order, product, and customer. The id is authored as a bare name (for example bulk-approve). Commerce handles prefixing and collision resolution when rendering the final Admin UI configuration.
Field applicability by variant
view onlyworker onlyidlabeltitledescriptionconfirmnotificationsselectionLimitaclProtectedpathsandboxPermissionsruntimeActiontimeoutThe view and worker variants are strict — setting path or sandboxPermissions on a worker action, or runtimeAction or timeout on a view action, fails validation.
idlabeltitleview) or confirmation surface (worker).descriptionapp-config for installation tooling.confirm{ title?, message? } confirmation dialog shown before the action runs.notifications{ success?, error? } banner messages shown in Commerce Admin after the action completes. When omitted, Commerce displays a default success or error banner.selectionLimitpath (view)#/export-orders.sandboxPermissions (view)allow-downloads, allow-modals, allow-popups.runtimeAction (worker)package/action path. Registered as a workerProcess operation automatically.timeout (worker)Mass action worker handler
import {
okMassActionResponse,
parseMassActionRequest,
} from "@adobe/aio-commerce-lib-admin-ui/mass-actions";
export async function main(params) {
const { gridType, selectedIds } = parseMassActionRequest(params);
await approveOrders(selectedIds);
return okMassActionResponse({ approved: selectedIds.length });
}
Use massActionErrorResponse(status, message) to report a failure. See the @adobe/aio-commerce-lib-admin-ui usage guide for the full wire contract, including the end-to-end ACL example.
View mass action page
A view mass action opens an iframe at path inside your App Builder frontend — there's no server-side handler. Read the selected row IDs and close the iframe with the React hooks exported from @adobe/aio-commerce-lib-admin-ui/web:
import {
useHostConnection,
useMassActionContext,
} from "@adobe/aio-commerce-lib-admin-ui/web";
import { Button, ComboBox, ComboBoxItem, Heading } from "@react-spectrum/s2";
import { style } from "@react-spectrum/s2/style" with { type: "macro" };
import { throwIfError } from "#web/utils.ts";
/** Lists the order IDs the mass action was triggered with, then closes the iframe on demand. */
export function MassActionWithRedirect() {
const { data } = throwIfError(useMassActionContext());
const { actions } = throwIfError(useHostConnection());
return (
<div className={style({ margin: 8 })}>
<Heading level={1}>Selected Ids</Heading>
<ComboBox defaultItems={data.selectedIds.map((id) => ({ id }))}>
{(item) => <ComboBoxItem id={item.id}>{item.id}</ComboBoxItem>}
</ComboBox>
<Button
onPress={actions.close}
styles={style({ marginTop: 8 })}
variant="primary">
Done
</Button>
</div>
);
}
Add order view buttons
Add buttons to the order detail page. As with mass actions, type selects view (iframe) or worker (runtime action):
adminUi: {
order: {
viewButtons: [
{
type: "view",
id: "delete-order",
label: "Delete",
description: "Permanently removes the order and its associated records.",
path: "#/delete-order",
level: 0,
sortOrder: 80,
sandboxPermissions: ["allow-modals", "allow-popups"],
confirm: { message: "Are you sure you want to delete this order?" },
},
{
type: "worker",
id: "sync-inventory",
label: "Sync inventory",
description: "Pushes the latest stock counts for this order's items to the ERP.",
runtimeAction: "orders/sync-inventory",
timeout: 15,
level: 1,
sortOrder: 10,
notifications: {
success: "Inventory synced successfully.",
error: "Inventory sync failed. Check the runtime logs.",
},
},
],
},
}
Order view buttons are only available on order.
Field applicability by variant
view onlyworker onlyidlabeldescriptionlevelsortOrderconfirmnotificationsaclProtectedpathsandboxPermissionsruntimeActiontimeoutThe view and worker variants are strict. Setting path or sandboxPermissions on a worker button, or runtimeAction or timeout on a view button, fails validation.
idlabeldescriptionapp-config for installation tooling.level-1 (left), 0 (center), or 1 (right).sortOrderlevel.confirm{ title?, message? } confirmation dialog shown before the handler runs.notifications{ success?, error? } banner messages shown in Commerce Admin after the handler returns. When omitted, Commerce displays a default success or error banner.path (view)#/delete-order.sandboxPermissions (view)allow-downloads, allow-modals, allow-popups.runtimeAction (worker)package/action path. Registered as a workerProcess operation automatically.timeout (worker)View order view button page
A view button opens an iframe at <extension-host>/index.html<path>?orderId=<orderId> inside your App Builder frontend. There is no server-side handler. Read the order ID and close the iframe with the React hooks exported from @adobe/aio-commerce-lib-admin-ui/web:
import { useHostConnection, useOrderViewButtonContext } from "@adobe/aio-commerce-lib-admin-ui/web";
export function DeleteOrderPage() {
const { orderId } = useOrderViewButtonContext();
const { close } = useHostConnection();
// ...delete orderId, then call close() to return to the order view.
}
Order view button worker handler
A worker button POSTs to your runtime action; parse the request and build the response with @adobe/aio-commerce-lib-admin-ui/order-view-buttons:
import {
okOrderViewButtonResponse,
parseOrderViewButtonRequest,
} from "@adobe/aio-commerce-lib-admin-ui/order-view-buttons";
async function syncInventory(orderId) {
// Your sync inventory logic for orderId
}
export async function main(params) {
const { id, orderId } = parseOrderViewButtonRequest(params);
await syncInventory(orderId);
return okOrderViewButtonResponse();
}
Use orderViewButtonErrorResponse(status, message) to report a failure. See the @adobe/aio-commerce-lib-admin-ui usage guide for the full wire contract.
ACL-protected extension points
Set aclProtected: true on a menu, grid column, mass action, or order view button to have Commerce generate a per-app ACL resource for that item and add it to the Adobe Commerce User Roles tree. Admins can then grant or deny the resource per role. Users without the resource do not see the item and cannot invoke it.
Each resource id follows a hierarchical scheme rooted at the app (derived from metadata.id), with a leaf id per protected item. Use the id helpers from @adobe/aio-commerce-lib-admin-ui instead of hardcoding the generated string:
getAclResourceId(metadataId) from /apigetMenuAclResourceId(metadataId, menuId) from /menugetGridColumnAclResourceId(metadataId, entity, columnId) from /grid-columnsgetMassActionAclResourceId(metadataId, entity, actionId) from /mass-actionsgetOrderViewButtonAclResourceId(metadataId, buttonId) from /order-view-buttonsCheck the resource from the runtime action handler with getAdminUiPermissionClient before serving protected content:
import {
AdminUiPermissionDeniedError,
getAdminUiPermissionClient,
} from "@adobe/aio-commerce-lib-admin-ui/api";
import { getMassActionAclResourceId } from "@adobe/aio-commerce-lib-admin-ui/mass-actions";
import { getCommerceClient } from "@adobe/aio-commerce-lib-app";
import { resolveImsAuthParams } from "@adobe/aio-commerce-lib-auth";
import appConfig from "#app.commerce.config";
export async function main(params) {
const appId = appConfig.metadata.id;
const httpClient = await getCommerceClient(resolveImsAuthParams(params));
const permissionClient = getAdminUiPermissionClient({ httpClient, appId });
try {
await permissionClient.require(
getMassActionAclResourceId(appId, "order", "bulk-approve"),
);
// Do something after checking permission is granted.
} catch (error) {
if (error instanceof AdminUiPermissionDeniedError) {
return massActionErrorResponse(403, "You do not have access to this action");
}
throw error;
}
}
See the Permission Client documentation for caching, deduplication, and the full end-to-end example.
After changing adminUi, rebuild and deploy your app so the pre-app-build hook refreshes generated artifacts. See Build and deploy for more information.
Related documentation
- Admin UI SDK — general Admin UI SDK concepts and extension points.
@adobe/aio-commerce-lib-admin-ui— wire contract builders, menu constants, and the permission client used bycommerce/backend-ui/2handlers.- Build and deploy — generated files and runtime actions for
commerce/backend-ui/2.