cx-composer
← blog12 min read

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).

referenceclient-extension.yamlliferay dxp

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).

❯ client-extension.yaml — minimal exampleyaml
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 via name_i18n.
  • description — free-form description. Also localizable via description_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 exampleyaml
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 into static. Files under static/ 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 — if true, the widget can appear multiple times on a page. Default false.
  • useESM — treat the JS as ES modules. Default false.
  • 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, or site. Default company.
  • scriptLocation (globalJS only) — head, bodyTop, or bodyBottom. Default bodyBottom.

###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):

  • .serviceAddress and .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 use localhost:<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.
❯ oAuthApplicationUserAgent — canonical shape (matches Liferay samples)yaml
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.

❯ oAuthApplicationHeadlessServer — canonical shapeyaml
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 the externalReferenceCode of 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's homePageURL to 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 paired oAuthApplicationHeadlessServer entry (whose .serviceAddress tells 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 (default true).

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.

─── skip the boilerplate ───

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.