client-extension.yaml Explained — Every Field, Every Type
A field-by-field reference for Liferay's client-extension.yaml manifest. What the top-level keys do, which fields every CX type requires, and the fields that trip developers up (assemble, oAuthApplicationUserAgent, resourcePath, url patterns).
client-extension.yaml is the manifest Liferay reads to install a Client Extension. It looks simple at first glance — a flat map of extension IDs — but every one of the 28 types has its own required and optional fields, and a few cross-cutting concepts (the assemble block, OAuth pairing, URL conventions) trip up almost everyone the first time. This is a field-by-field reference for the manifest as of Liferay DXP 2024.Q3.
##1. The shape of the manifest
A client-extension.yaml file is a top-level YAML map. Every key is the ID of one extension; every value is the extension's configuration. One file can declare as many extensions as you want, and they can be different types — this is how you pair a microservice extension with its OAuth application (see cross-references below).
my-widget:
type: customElement
name: "My Widget"
description: "A little widget"
htmlElementName: my-widget
urls: ["index.js"]
assemble:
- from: assets
into: static
Two things to notice. First, every real Liferay sample uses four-space indent — SnakeYAML (Liferay's parser) accepts two-space indent too, but tabs are always a hard error and the whole ecosystem defaults to four spaces, so match the samples. Second, the assemble: key at the top level is not itself an extension; it's a directive that tells the build how to package files into the .zip.
##2. Fields every type shares
Regardless of type, three fields apply to every extension:
type— the extension type. This is the switch that tells Liferay which internal handler to route this entry to. Full list of 28 accepted values in the sections below.name— human-readable display name. Shows up in the admin UI (System Settings, OAuth 2 Administration, Object Actions dropdown). Localizable vianame_i18n.description— free-form description. Also localizable viadescription_i18n.
Every other field on an extension is type-specific and lives under either the extension's root or its typeSettings: block, depending on the type. Frontend types (Custom Element, Global CSS/JS) tend to put fields at the root; backend and configuration types tend to nest them under typeSettings:.
##3. The assemble block
The assemble: block controls what files end up in the deployed .zip. It's a list of source→target mappings:
assemble:
- from: assets # source folder in your extension project
into: static # target folder inside the built .zip
- from: build/static
into: static
Different extension families expect different into: targets:
- Static-asset types (
globalJS,globalCSS,customElement,themeCSS,themeSpritemap, JS Import Map Entry) → assemble intostatic. Files understatic/become accessible at/o/<extension-id>/<filename>once deployed. - Site Initializer → assemble into
site-initializer. The full folder tree (layouts/,fragments/,object-definitions/, etc.) lives inside there. - Microservice types (Object Action, Workflow Action, CAPTCHA, Commerce) → skip the assemble block entirely. The build packages the whole project folder (Dockerfile + source) as the extension.
##4. Frontend types
###customElement
Registers a web component with Liferay so it can be dropped onto pages via the page editor. Fields at the extension root:
htmlElementName— the custom element tag name (must contain a hyphen; e.g.,my-widget). Required.urls— array of JS files (relative to the assemble target). Required.cssURLs— array of CSS files. Optional.instanceable— iftrue, the widget can appear multiple times on a page. Defaultfalse.useESM— treat the JS as ES modules. Defaultfalse.portletCategoryName— where the widget appears in the page editor sidebar (e.g.,category.marketing).
###globalJS / globalCSS
Injects JS or CSS globally, on every page in a scope. Fields:
urls— file paths, relative to the assemble target. Required.scope—portalscope,company, orsite. Defaultcompany.scriptLocation(globalJS only) —head,bodyTop, orbodyBottom. DefaultbodyBottom.
###themeCSS
Overrides a theme's Clay CSS variables. Fields:
clayURL— path to the compiled Clay CSS override file.mainURL— path to the theme's main CSS.frontendTokenDefinitionJSON— path to the JSON file describing the theme's design tokens (colors, spacing, typography). Required for the theme to be editable in Liferay's style-book UI. See cross-references below.
###Other frontend types
themeSpritemap (SVG spritesheet), themeFavicon, jsImportMapsEntry (bundled JS module registered in the import map), fdsCellRenderer / fdsFilter (custom renderers for Frontend Data Set widgets), and staticContent (arbitrary static files served under /o/<id>). All follow the same urls-and-assemble pattern.
##5. Configuration types
###oAuthApplicationUserAgent
Provisions an OAuth 2.0 client for the user-agent flow — used by Object Actions, Workflow Actions, Notification Types, Object Validation Rules, CAPTCHA, and every Commerce integration. Every real sample in liferay/liferay-portal uses the same five-field shape (verified against liferay-sample-etc-node and liferay-sample-etc-spring-boot):
.serviceAddressand.serviceScheme— dot-prefixed keys are local-dev metadata read by the Liferay Workspace Gradle plugin. They tell the workspace where your microservice runs. Real samples always uselocalhost:<port>; the workspace / Liferay Cloud remaps the address at deploy time via config trees.name— display name shown in OAuth 2 Administration.scopes— list of Liferay REST scopes your service can invoke (e.g.,Liferay.Headless.Admin.Workflow.everything).type: oAuthApplicationUserAgent.
my-service-oauth-agent:
.serviceAddress: localhost:3001
.serviceScheme: http
name: My Service OAuth Application User Agent
scopes:
- Liferay.Headless.Admin.Workflow.everything
type: oAuthApplicationUserAgent
###oAuthApplicationHeadlessServer
Same five-field shape as user-agent, but for headless-server flows used by Batch and Site Initializer — where the paired service calls into Liferay rather than Liferay dispatching to it. Because Liferay is the target, .serviceAddress points at Liferay's port ( localhost:8080 in every sample) instead of the microservice's.
my-batch-oauth-server:
.serviceAddress: localhost:8080
.serviceScheme: http
name: My Batch OAuth Application Headless Server
scopes:
- Liferay.Headless.Admin.Workflow.everything
- Liferay.Headless.Batch.Engine.everything
- Liferay.Object.Admin.REST.everything
type: oAuthApplicationHeadlessServer
###Other configuration types
instanceSettings (system-wide settings surfaced in System Settings), notificationTemplate (a notification template targetable by notification types), and portalProperties (portal-wide property overrides). Each has a small, self-contained field set.
##6. Microservice types
These types register an external HTTP service as a callable extension. Two top-level fields (not under typeSettings) are shared across every microservice type:
oAuth2ApplicationExternalReferenceCode— must equal theexternalReferenceCodeof a paired OAuth application declared elsewhere (same yaml is easiest). Note the 2 in the field name.resourcePath— the path your handler is mounted on. Must start with/. Liferay appends this to the paired OAuth application'shomePageURLto derive the URL it dispatches to.
The specific types:
objectAction— fires on Object entry lifecycle events (create/update/delete/manual trigger).workflowAction— fires as a step in a Kaleo workflow definition.notificationType— a new delivery channel for Liferay notifications (Slack, Discord, custom email service, etc.).objectValidationRule— validates an Object entry synchronously before it's saved. Your service returns whether the entry is valid.objectEntryManager— replaces Liferay's default Object entry storage with your own backend (rare, powerful).fdsCellRenderer,fdsFilter— custom renderers/filters for Frontend Data Set widgets (these are frontend types but can be paired with a microservice for backend-computed values).- Commerce integrations:
orderPaymentMethod,orderShippingMethod,orderCheckout,orderTaxMethod. Each maps to a step in Liferay Commerce's checkout. captcha— a custom CAPTCHA provider (e.g., wire in Cloudflare Turnstile instead of Liferay's built-in ReCAPTCHA).
##7. Batch and Site Initializer
###batch
Registers a headless-batch-engine import unit. Your service exposes JSON files describing objects to create (users, sites, organizations, whatever headless supports) and Liferay imports them on extension deploy. Fields:
oAuthApplicationHeadlessServer— the yaml key of a pairedoAuthApplicationHeadlessServerentry (whose.serviceAddresstells Liferay where to fetch from).
###siteInitializer
The biggest and most complex type. A siteInitializer provisions an entire site — layouts, master pages, fragments, taxonomies, style books, object definitions, roles, navigation menus — from a folder tree. Fields:
oAuthApplicationExternalReferenceCode— headless-server OAuth pairing.thumbnail— path to a thumbnail image shown in the site creation UI.ready— whether the initializer is deployable (defaulttrue).
The content of the site — every layout, fragment, taxonomy, etc. — lives in the site-initializer/ folder alongside the yaml. The folder structure is strict: schemas verified against Liferay's liferay-aicontentwizard-site-initializer and site-initializer-masterclass reference samples.
##8. Cross-references
###OAuth pairing
Whenever a microservice extension references oAuth2ApplicationExternalReferenceCode: some-oauth-id, a paired OAuth application with a matching externalReferenceCode must exist somewhere Liferay can find it — either in the same file (recommended) or provisioned separately in the admin UI. If the reference can't be resolved at deploy time, the extension deploys but its runtime calls all 401 because there's no client to authenticate with. Batch and Site Initializer use the oAuthApplicationHeadlessServer field instead (for the headless-server flow).
CX Composer's validator catches this at edit time: right-click your client-extension.yaml, run Validate Whole CX (folder), and any unresolved OAuth reference surfaces in the Problems panel.
###frontendTokenDefinitionJSON
On a themeCSS extension, this field points at a JSON file describing the theme's design tokens. The file has to exist relative to the extension folder and has its own strict schema (categories → sets → tokens → mappings, with known editor types like ColorPicker, Length, FontFamily). CX Composer's smart validator walks up from afrontend-token-definition.json file to find its parent theme extension's yaml, then validates the JSON only if it's actually referenced.
##9. Common validation errors
###Unknown field "xxx" for type "yyy"
You typed a field name wrong or you're using a field from a different type. Every type has a fixed set of allowed fields. CX Composer surfaces did-you-mean suggestions for unknown fields based on Liferay's official schema.
###YAMLException: bad indentation
Tabs, or two-space indent. Liferay expects four-space indent. Convert withexpand -t 4 or configure your editor to use spaces for YAML files.
###OAuth2ApplicationHomePageURLSchemeException
Almost always thrown by a site initializer at deploy time. It means your site-initializer/ folder references a URL scheme (in a portlet preference, a fragment configuration, or an OAuth entry) that Liferay doesn't recognize. Common cause: leaving a http://localhost:8080 URL in a fragment's index.json after copying from a sample.
###LayoutNameException
Site initializer error: one of your layouts/<n>/page.json files has a missing or non-localizable name_i18n field. Every layout must declare name_i18n with at least the en_US key.
###Missing companion file
A folder-level validation error (surfaced by CX Composer's Validate Whole CX). Common culprits: a microservice extension without a Dockerfile, a static-asset type whose urls reference files that don't exist in the assemble source, or a themeCSS declaring afrontendTokenDefinitionJSON that points at a missing file.
Scaffold this in 10 seconds with CX Composer.
VS Code extension for Liferay developers. Wizard-driven scaffolding for all 28 Client Extension types, IntelliSense on client-extension.yaml, five layered validators, and per-extension build via Blade.