plugins/commerce/app-management/skills/commerce-app-storage/SKILL.md
Integrate App Builder Database Storage (@adobe/aio-lib-db) into an Adobe Commerce app and scaffold a runtime action that reads and writes documents. Use when the user wants persistent, queryable storage backing a Commerce app — either from a web action (HTTP-invokable) or from an event/webhook handler. Requires a base app initialized with commerce-app-init.
npx skillsauth add adobe/skills commerce-app-storageInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Integrates App Builder Database Storage into an existing Commerce app and scaffolds a runtime action that uses @adobe/aio-lib-db to read and write documents. The library is MongoDB-like: data lives in collections of documents, queried with familiar filters.
The db-access code is identical regardless of action type — what differs is how the action is registered and what its handler returns:
web: "yes"); returns a response built with the responses helpers from @adobe/aio-commerce-lib-core.app.commerce.config.ts via commerce-app-eventing (runtimeActions) or commerce-app-webhooks (runtimeAction).app.commerce.config.ts present in the project root, andsrc/commerce-extensibility-1/ directory and installed node_modules (the @adobe/aio-commerce-lib-app dependency).app.commerce.config.ts is missing, stop and invoke commerce-app-init first (it writes the config, then runs init).src/commerce-extensibility-1/ or node_modules), run npx @adobe/aio-commerce-lib-app init before continuing. Init is idempotent — it finds the existing config, skips the interactive prompts, installs dependencies, and generates the project files.AppBuilderDataServicesSDK) must be added to the project in the Adobe Developer Console — in every workspace that uses the database (no special license beyond App Builder). Without it, runtime actions cannot authenticate to the database service.There is a strict one-to-one relationship between an AIO project workspace and a workspace database. The recommended way to provision it is declaratively in app.config.yaml — the database is provisioned (if not already present) on aio app deploy:
application:
runtimeManifest:
database:
auto-provision: true
region: emea # amer | apac | emea | aus — the single source of truth for the region
Extension-only apps need a workaround. Due to a bug in the aio app CLI plugin (not aio-lib-db), aio app deploy only runs declarative auto-provision when the application runtime manifest has at least one package with a runtime action. Apps built purely with extensions (the recommended layout per the submission guidelines) have no application actions, so deploy silently skips provisioning. Make the application block "real enough" for provisioning to run by adding an empty packages map and a post-app-build hook that creates the directory the provisioning step expects:
application:
hooks:
post-app-build: "mkdir -p dist/application/actions" # provisioning expects this dir to exist
runtimeManifest:
packages: {} # empty map — required by the config schema so the application block validates with no actions
database:
auto-provision: true
region: emea # single source of truth — see the region callout below
For local development, declarative auto-provisioning does not run during aio app run / aio app dev. Provision once up front with the CLI fallback (self-service, no special permissions). The aio app db … commands are only available once the storage CLI plugin is installed:
aio plugins install @adobe/aio-cli-plugin-app-storage
aio app db provision --region <amer|apac|emea|aus>
Region is a single source of truth. The
regionin the manifestdatabaseblock must match theregionpassed to everyinitDb({ region })call (orAIO_DB_REGION) — in every action and in the install step (Step 6). A mismatch fails the connection. Changing region is destructive:aio app db delete, updatedatabase.regionin the manifest, then re-provision.
npm install @adobe/aio-lib-db
Gather from the user:
database.region (see the region callout in Step 1). Pass it to init() or set AIO_DB_REGION.Add the action to a user-defined package in src/commerce-extensibility-1/ext.config.yaml (any name except app-management, which is reserved).
include-ims-credentials: trueis required on every DB action. Without it,aio-lib-dbhas no IMS token to authenticate with and the connection fails at runtime (and the app installation fails if the action runs during install). Do not omit this annotation.
# src/commerce-extensibility-1/ext.config.yaml
runtimeManifest:
packages:
app-management:
# ... auto-generated — do not edit
my-app: # any name except "app-management"
actions:
store-record:
function: actions/store-record/index.js # relative to src/commerce-extensibility-1/
runtime: nodejs:24
web: "yes" # "yes" for a web action; "no" for an event/webhook action
annotations:
include-ims-credentials: true # REQUIRED for aio-lib-db auth
| Field | Constraint |
| ------------------------- | --------------------------------------------------------------------------------- |
| Package name | Lowercase alphanumeric + hyphens; never app-management (reserved) |
| function | Path relative to src/commerce-extensibility-1/ — not src/... or root-relative |
| include-ims-credentials | Must be true — without it init() has no IMS token and the connection fails |
| web | "yes" for HTTP-invokable web actions; "no" for event/webhook handlers |
| Collection name | Non-empty string; created on first write if it doesn't exist |
| Region | Must match the manifest database.region (amer | apac | emea | aus) |
Every handler follows the same lifecycle: resolve IMS auth → mint token → init → connect → use a collection → always close in finally.
// Web action — src/commerce-extensibility-1/actions/store-record/index.ts
import { buildErrorResponse, ok } from "@adobe/aio-commerce-lib-core/responses";
import {
getImsAuthProvider,
resolveImsAuthParams,
} from "@adobe/aio-commerce-lib-auth";
import { init as initDb } from "@adobe/aio-lib-db";
export async function main(params: Record<string, unknown>) {
let client;
try {
// Resolve the injected AIO_COMMERCE_AUTH_IMS_* params, then mint a raw token string.
const authProvider = getImsAuthProvider(resolveImsAuthParams(params));
const token = await authProvider.getAccessToken();
const db = await initDb({ token, region: "emea" }); // must match the manifest database.region
client = await db.connect();
const records = client.collection("records");
const result = await records.insertOne({
...(params.document as object),
createdAt: new Date().toISOString(),
});
return ok({ body: { result } });
} catch (error: any) {
return buildErrorResponse(error.statusCode || 500, {
body: { message: error.message },
});
} finally {
if (client) await client.close(); // always close — avoids connection leaks
}
}
For an event/webhook action the only differences are web: "no" in registration and that the payload arrives in params.data:
// Event/webhook handler — same init/connect/close lifecycle
export async function main(params: Record<string, unknown>) {
const data = params.data as Record<string, unknown>;
let client;
try {
const authProvider = getImsAuthProvider(resolveImsAuthParams(params));
const token = await authProvider.getAccessToken();
const db = await initDb({ token, region: "emea" });
client = await db.connect();
await client
.collection("orders")
.insertOne({ orderId: data.order_id, receivedAt: new Date() });
return ok({ body: { processed: true } });
} finally {
if (client) await client.close();
}
}
See assets/db-action.ts for the full annotated reference covering all CRUD operations and cursor iteration.
For an App Management app, create collections and indexes with a custom installation step — a script that runs once when the app is installed from the Commerce Admin, and can be reversed on uninstall. Prefer this over creating them ad-hoc on the first request.
Author the step with defineCustomInstallationStep (an install handler plus an optional uninstall). Inside it, resolve the IMS auth params from context.params — not config — then follow the same init → connect → close lifecycle as Step 5, and call createIndex on the collection object:
// ./scripts/setup-database.ts — referenced from config as ./scripts/setup-database.js
import { defineCustomInstallationStep } from "@adobe/aio-commerce-lib-app/management";
import {
getImsAuthProvider,
resolveImsAuthParams,
} from "@adobe/aio-commerce-lib-auth";
import { init as initDb } from "@adobe/aio-lib-db";
export default defineCustomInstallationStep({
install: async (config, context) => {
let client;
try {
// context.params carries the injected IMS credentials — NOT config.
const authProvider = getImsAuthProvider(
resolveImsAuthParams(context.params),
);
const token = await authProvider.getAccessToken();
const db = await initDb({ token, region: "emea" }); // must match the manifest database.region
client = await db.connect();
const orders = client.collection("held_orders"); // get the collection object first
await orders.createIndex({ order_id: 1 }, { unique: true }); // createIndex on the collection, not a name string
return { status: "success" };
} finally {
if (client) await client.close(); // always close — avoids connection leaks
}
},
uninstall: async (config, context) => {
// Tear down your database state here.
// Leave empty to preserve data across reinstalls.
},
});
Author the install script as an ES module with
export default— nevermodule.exports. The installation action loads each step viaimport * as step from "<script>"and readsstep.default, so the script must default-export thedefineCustomInstallationStep(...)result. CommonJS breaks this:module.exports.defaultsurfaces asstep.default.defaultand validation fails. Thescriptpath must end in.js; if you author in TypeScript, compile it and keep the emitted.jsan ES module.
Register the step in app.commerce.config.ts under installation.customInstallationSteps. The script path points at the compiled .js output:
// app.commerce.config.ts
installation: {
customInstallationSteps: [
{
script: "./scripts/setup-database.js", // compiled output of setup-database.ts
name: "Set up held-orders collection",
description: "Creates the held_orders collection and a unique index on order_id",
},
],
},
| Field | Constraint |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| script | Path relative to the project root; must be an ES module (export default) ending in .js (compile from TS if you author it that way) |
| name | Non-empty string, ≤ 255 characters; unique across all installation steps |
| description | Non-empty string, ≤ 255 characters |
See assets/setup-database.ts for the full annotated install/uninstall reference.
aio app build
A build failure points directly to the offending config field. To exercise the action against the real database, deploy and invoke it (aio app deploy).
finally block — leaked connections exhaust resources.database.region is the single source of truth; every init() call and the install step must use it (see the region callout in Step 1). A mismatch fails the connection silently from the caller's view..project({ field: 1 })) and indexes (createIndex) for frequently queried fields; index fields must total ≤ 2048 bytes.for await (const doc of collection.find(...))) instead of toArray() to bound memory.AIO_DB_REGION and the injected IMS token over inline values.@adobe/aio-sdk umbrella — e.g. @adobe/aio-commerce-lib-auth for IMS auth and @adobe/aio-lib-core-logging for the logger — to keep action bundles small.defineCustomInstallationStep, see Step 6) rather than ad-hoc on the first request — it runs once when the app is installed from the Commerce Admin and is reversible on uninstall. A generic App Builder post-app-deploy hook is only an alternative when the app is not installed through App Management.include-ims-credentials: true, or the App Builder Data Services API has not been added to the project in the Adobe Developer Console (see Prerequisites).aio app deploy (extension-only app): a bug in the aio app CLI plugin (not aio-lib-db) skips declarative auto-provision when the application runtime manifest has no runtime action. Apply the extension-only workaround from Step 1 — add packages: {} and a post-app-build: "mkdir -p dist/application/actions" hook under application — or provision once with the CLI fallback for local dev.database.region. Moving regions is destructive — aio app db delete, update database.region in the manifest, then re-provision (aio app deploy, or the CLI fallback for local dev)._id from a string returns nothing: convert it first — new ObjectId(idString) from bson. A raw string never matches the stored ObjectId.DbError vs unexpected error: errors thrown by the service have name === "DbError"; branch on it to separate database failures from application bugs.context.params (resolveImsAuthParams(context.params)) — which carries the injected AIO_COMMERCE_AUTH_IMS_* credentials — not from config, which holds no credentials. Use @adobe/aio-commerce-lib-auth, not @adobe/aio-lib-core-auth: the latter's generateAccessToken expects clientId/clientSecret directly and cannot consume the injected params.must export a default function or object): the script was authored as CommonJS. Author it as an ES module with export default; module.exports (or module.exports.default) surfaces through the framework's import * as loader as .default.default and fails validation.createIndex errors or has no effect: it must be called on a collection object (client.collection("name").createIndex({ field: 1 })), not with a collection-name string. Get the collection first, then call createIndex on it.aio app build completes without errorsinclude-ims-credentials: true in its annotationsfinally block and initializes the library in the region declared in the manifest database blockcommerce-app-eventing and reference this action in an event's runtimeActions.commerce-app-webhooks and reference this action via runtimeAction.commerce-app-admin-ui to add a mass action, order view button, or grid column that invokes this runtime action.context.params and createIndex-on-collection patternstools
Use the run-workflow MCP to discover, compose, execute, publish, and save Adobe Firefly workflows. TRIGGER when: user asks what actions are available, what the MCP can do, how to process images/video/3D via workflow, wants to build/run/save/publish a workflow, OR pastes any workflow/batch/execution ID. BARE ID (UUID/workflowId/batchId) = INSPECT ONLY — call inspect_run, NEVER run_workflow_submit. ALWAYS call list_actions first for capability/discovery questions. DO NOT TRIGGER for direct Firefly API calls without MCP (use firefly-api-specs).
tools
Run predefined featured workflows via run-workflow MCP. TRIGGER when user names a featured workflow (retargeting, banners at scale, localization, packaging, banner advertising, etc.) or asks to run a known marketing/production workflow. Requires run-workflow MCP. ALWAYS call get_featured_workflow before compose_workflow. DO NOT TRIGGER for custom one-off workflows with no named template — use run-workflow skill.
tools
Migrate an Adobe Commerce App Builder project from the Integration Starter Kit or Checkout Starter Kit to the new App Management approach. Run from the root of the App Builder project to be migrated. Pass --auto to skip confirmation prompts (suitable for CI or batch use) — auto mode prints a summary of all Q&A questions answered with their defaults. Pass --doc-scan-only to scan README.md and env.dist for outdated content without modifying any files. Use when the user wants to migrate an App Builder project from the Integration Starter Kit or Checkout Starter Kit to the App Management approach, or mentions upgrading their Adobe Commerce extension architecture.
development
Add or modify webhook interceptors in an Adobe Commerce app. Use when the user wants to intercept Commerce operations to validate input, append data, or modify behavior — before or after execution. Requires a base app initialized with commerce-app-init.