FFCPE App Builder actions (@adobe/ffcpe-custom-node-core, @adobe/ffcpe-custom-node-app-builder)
Use this skill when implementing or extending Workflow Builder custom actions in an Adobe App Builder (OpenWhisk) repo with @adobe/ffcpe-custom-node-core and @adobe/ffcpe-custom-node-app-builder.
For HTTP field-level contract details, see docs/custom-action-requests.md in this SDK repo. For consumer install and examples, see README.md. For package install, local linking, and security guidance, see ffcpe-custom-node-sdk. For catalog-entry.json and aio ffcpe catalog, use ffcpe-catalog-entry-json and aio-ffcpe-cli from adobe/aio-cli-plugin-ffcpe:
npx skills add adobe/aio-cli-plugin-ffcpe --all -y
Packages and peers
@adobe/ffcpe-custom-node-core— contract types,handleFfcpeSubmit/handleFfcpeStatus,runWorkerJob, input/output helpers, IMS inbound auth helpers.@adobe/ffcpe-custom-node-app-builder—mountFfcpeNodeRoutes,createFfcpeNodeWorker, AIOJobStore, OpenWhisk orchestrator,buildStatusUrl,createAioLogger.
Declare hono as a peer (your app chooses the version). Typical App Builder peers also include @adobe/aio-lib-state, @adobe/aio-lib-ims, @adobe/aio-lib-core-logging, and openwhisk — align versions with your template’s package.json.
Web action (Hono)
- Import
Hono,ToOpenWhiskActionfromhono-openwhisk-adapter, andmountFfcpeNodeRoutesfrom@adobe/ffcpe-custom-node-app-builder. const app = new Hono(), thenmountFfcpeNodeRoutes(app, { worker: { package, name }, web: { package, name } }).webandworkermust match OpenWhisk package and action names in your manifest (forstatusUrland async invoke).- Default routes are
POST /submitandGET /status; omitroutesunless paths differ from defaults. export const main = ToOpenWhiskAction(app)(or the entry shape your bundler expects).
The web action in *.config.yaml / ext.config.yaml must use web: "raw" and require-adobe-auth: false, plus a supported Node runtime (e.g. nodejs:22):
runtimeManifest:
packages:
my-ffcpe-app:
actions:
my-web-action:
function: actions/my-web-action.ts
web: "raw"
runtime: nodejs:22
annotations:
require-adobe-auth: false
my-worker:
function: actions/my-worker.ts
runtime: nodejs:22
Worker action
- Implement
FfcpeNodeWorkerHandler:ctxhasjobId,inputs(FfcpeInput[]),params(submitparameters),logger, optionalauthContext,args(fullmain(args)). - Return
{ status: "completed", outputs?: Output[] }or{ status: "failed", error: string }. UsecreateTextOutput,createImageOutput,createVideoOutput,createJsonOutput. export const main = createFfcpeNodeWorker(handler, options?).requiredInputNamesrefers to each input’sport, not the displayname.
Use getTextInput, getImageInput, etc. with ctx.inputs (helpers take the inputs array, not the full submit body).
Manifest inputs (secrets): usually environment variables — use process.env, not ctx.inputs. Do not log secrets.
Starting from scratch with init-bare
If you have no App Builder project yet, the fastest path is aio app init --standalone-app (or the init-bare script wrapper). Warning: this generator produces more than a bare skeleton — it scaffolds web-src/, actions/generic/, actions/publish-events/, test/, and e2e/ that are irrelevant to a headless FFCPE action. Follow these steps in order:
1. Create the Console project and workspace
# Create the project
aio console project create -n myproject -t "My Project Title" --json
# Stage workspace is created automatically; select it (use the project ID from the create output)
aio console project select myproject
aio console workspace select Stage --projectId <PROJECT_ID>
Console project name constraints (from real setup failures):
| Rule | Detail |
|---|---|
| Alphanumeric only | Hyphens and special characters are rejected (demo-qr-custom-ffcpe-nodes → Project name … is invalid). Use camelCase or a short slug: demoQrFfcpe. |
| Max 20 characters | Longer names fail with Project name length must be less than 20. |
| Name ≠ OpenWhisk package | The Console project name (e.g. demoQrFfcpe) can differ from the runtimeManifest package name in app.config.yaml (e.g. demo-qr-custom-ffcpe-nodes). Hyphens are fine in the YAML package and action names. |
| Stage auto-created | Production and Stage workspaces are created with the project; you usually do not need aio console workspace create for a first deploy. |
Console CLI flag reference — flags differ by command; mismatches throw NonExistentFlagsError:
| Command | Key flags |
|---|---|
aio console workspace create |
--projectName <name> (required), --name <name> (required) |
aio console workspace select |
positional [WORKSPACEIDORNAME] + --projectId <id> |
aio console workspace list |
--projectId <id> (not --projectName) |
aio console workspace download |
positional [DESTINATION] + --projectId <id> |
2. Initialise the project
aio app init -y --no-login --standalone-app --no-install
3. Wire the local app to the Console workspace
aio app use --no-input alone fails after init-bare because the local .aio file has no org/project/workspace context yet. The correct sequence is:
# Download the workspace credentials/config from Console
aio console workspace download # saves e.g. <orgId>-<project>-Stage.json
# Import that config — this populates .aio and .env
aio app use <orgId>-<project>-Stage.json --overwrite --no-input
If .env already exists (common after aio app init), a bare aio app use <file>.json prompts interactively and can hang in non-interactive/agent sessions. Always pass --overwrite --no-input when importing a downloaded workspace config into a scaffolded project.
The downloaded JSON file contains secrets (client credentials, API keys). Add it to .gitignore immediately after downloading — before any git add. The generated .gitignore does not cover this file automatically:
# Add the specific file, or use the pattern [0-9]*-*-*.json to cover all workspaces
echo '<orgId>-<project>-Stage.json' >> .gitignore
Also confirm .env is ignored (the generated .gitignore covers this via .env*, but verify it is present).
4. Clean up the scaffolded files
The generator creates files that conflict with a headless FFCPE action. Remove them:
rm -rf web-src actions/generic actions/publish-events test e2e
5. Fix app.config.yaml
The generated YAML includes web: web-src at the application level, which causes the frontend build step to run (and fail) on a headless project. Remove that line and replace the scaffolded actions with your FFCPE pair:
application:
actions: actions
runtimeManifest:
packages:
my-ffcpe-app:
license: Apache-2.0
actions:
my-action-web:
function: actions/my-action/my-action.web.ts
web: "raw"
runtime: nodejs:22
annotations:
require-adobe-auth: false
my-action-worker:
function: actions/my-action/my-action.worker.ts
runtime: nodejs:22
inputs:
LOG_LEVEL: debug
6. Replace webpack-config.js
The generator emits a config that uses ts-loader and is missing libraryTarget: "commonjs2". Replace it entirely (not a merge — the generated output section is wrong):
// webpack-config.js
module.exports = {
output: {
libraryTarget: 'commonjs2',
},
module: {
rules: [
{
test: /\.[jt]sx?$/,
loader: 'esbuild-loader',
options: { target: 'es2020' },
},
],
},
resolve: {
extensions: ['.ts', '.js'],
},
}
Install the loader:
npm install -D esbuild-loader
# or: pnpm add -D esbuild-loader
7. Install FFCPE packages
npm view @adobe/ffcpe-custom-node-core version
npm view @adobe/ffcpe-custom-node-app-builder version
npm view hono-openwhisk-adapter version
npm install @adobe/ffcpe-custom-node-core@latest @adobe/ffcpe-custom-node-app-builder@latest hono hono-openwhisk-adapter@latest
Use the latest published versions — run npm view <package> version before install (or install with @latest as above). Do not assume or pin stale ranges such as 0.1.x; those may not exist on the registry. Omitting a version also resolves to latest, but @latest makes intent explicit in docs and scripts. After install, package-lock.json pins exact versions for reproducibility.
Image outputs: createImageOutput expects an HTTPS URL, not raw bytes. Upload with @adobe/aio-lib-files (init(), write(), generatePresignURL()) — see the thumbnail example in the SDK README.md.
Multiple custom actions in one app
Each FFCPE node needs its own web + worker pair and co-located <action-name>.entry.json. Declare all pairs under one runtimeManifest package (or split across packages if your org prefers). Example with two nodes:
runtimeManifest:
packages:
my-ffcpe-app:
actions:
generate-foo-web: { function: actions/generate-foo/generate-foo.web.ts, web: "raw", … }
generate-foo-worker: { function: actions/generate-foo/generate-foo.worker.ts, … }
decode-foo-web: { function: actions/decode-foo/decode-foo.web.ts, web: "raw", … }
decode-foo-worker: { function: actions/decode-foo/decode-foo.worker.ts, … }
Each .web.ts calls mountFfcpeNodeRoutes with its own worker.name / web.name matching the YAML action keys.
Adding actions to an existing App Builder repo
1. Co-located files (recommended)
For each custom action, keep web, worker, and catalog entry JSON in the same directory, sharing the same <action-name> prefix:
<action-name>.web.tsor.js— Hono submit/status web action<action-name>.worker.tsor.js— async worker<action-name>.entry.json— run-workflow catalog entry (same document you pass toaio ffcpe catalog validate/register)
Example layout:
actions/
resize-image/
resize-image.web.ts
resize-image.worker.ts
resize-image.entry.json
The .entry.json file is not a separate template artifact — it is the catalog registration payload (handlerType: "custom-action", ports, customActionConfig, discovery fields). Author it with skill ffcpe-catalog-entry-json.
Flat co-location (same folder, no subdir) is fine if your repo already uses actions/resize-image.web.ts next to actions/resize-image.worker.ts — still name the catalog file resize-image.entry.json beside them.
2. *.config.yaml
Update runtimeManifest (or equivalent): declare the web action with web: "raw" and annotations.require-adobe-auth: false, plus the worker action; function paths must match your build outputs. mountFfcpeNodeRoutes package / name must match this YAML.
3. TypeScript + webpack (CJS for dist)
Install:
npm install -D esbuild-loader
# or
pnpm add -D esbuild-loader
If the project was initialized with init-bare, replace webpack-config.js entirely — the generated file uses ts-loader and lacks libraryTarget: "commonjs2". For other existing projects, merge into *webpack-config.js (preserve existing rules via webpack-merge or careful Object.assign):
output: {
libraryTarget: "commonjs2", // Important for CommonJS output
},
module: {
rules: [
{
// Match `.js`, `.jsx`, `.ts` or `.tsx` files
test: /\.[jt]sx?$/,
loader: "esbuild-loader",
options: {
// JavaScript version to compile to
target: "es2020",
},
},
],
},
resolve: {
extensions: [".ts", ".js"], // Add .ts and .js to the list of extensions to resolve
},
Deployed Runtime bundles are CommonJS; source can still be ESM-style TypeScript.
Register in the catalog (after deploy)
Deploy web + worker actions:
aio app build aio app deployDeploy output lists web action base URLs, e.g.:
https://3326322-myproject-stage.adobeioruntime.net/api/v1/web/my-ffcpe-app/my-action-webDerive catalog endpoints from that base URL (default Hono routes from
mountFfcpeNodeRoutes):Field Pattern submitEndpoint{web-action-base-url}/submitstatusEndpoint{web-action-base-url}/statusUse
/api/v1/web/(not/apis/v1/). The Runtime namespace in the hostname is lowercase (e.g.3326322-demoqrffcpe-stageeven when the Console project isdemoQrFfcpe).Fill in
<action-name>.entry.json—handlerType: "custom-action", port names matching the worker, endpoints from step 2, andcustomActionConfig.authentication. When the web action uses defaultmountFfcpeNodeRoutesauth (noauthenticate: null), set"authentication": { "type": "ims_service_token" }— notnone. Seeffcpe-catalog-entry-json.Install and auth the CLI plugin:
npm install -g @adobe/aio-cli aio plugins:install @adobe/aio-cli-plugin-ffcpe aio login aio console org selectValidate and register — do not use raw curl (path is the co-located
.entry.json):aio console org select # required before catalog API calls aio ffcpe catalog validate --file ./actions/resize-image/resize-image.entry.json aio ffcpe catalog register --file ./actions/resize-image/resize-image.entry.jsonIf the action type already exists, use
aio ffcpe catalog update <actionType> --file …instead ofregister.
Agent checklist
- New project: ran
init-bare→ downloaded workspace config →aio app use <file>.json --overwrite --no-input→ removedweb-src/,actions/generic/,actions/publish-events/,test/,e2e/. - Console project name: alphanumeric, ≤20 chars; may differ from
runtimeManifestpackage name. app.config.yaml: noweb: web-srcline; FFCPE web + worker pairs declared (one pair per custom action).webpack-config.js:libraryTarget: "commonjs2",esbuild-loader,.ts/.jsextensions — replace theinit-baregenerated file, don't merge.- Web + worker names align with
mountFfcpeNodeRoutesand YAML. - Web:
web: "raw",annotations.require-adobe-auth: false. - Worker: FFCPE data from
ctx.inputs; secrets fromprocess.envonly; image outputs viaaio-lib-filespresigned URLs. - Catalog:
<action-name>.entry.jsonco-located; port names match worker ports;submitEndpoint/statusEndpointcopied from deploy output +/submit//status; authims_service_tokenwhen using default route auth;aio ffcpe catalog validatethenregister(orupdate).