DEV Community

Cover image for Stop hand-writing Liferay Client Extensions
CX Composer
CX Composer

Posted on

Stop hand-writing Liferay Client Extensions

A VS Code plugin that scaffolds every one of the 28 types, catches typos before deploy, and generates full Node.js / TypeScript microservice starters. Free tier for validation.


If you've built anything on Liferay DXP in the last two years, you've touched client-extension.yaml. It's how Liferay's platform composes UI, backend integrations, and configuration on top of DXP without shipping an OSGi bundle.

The concept is beautiful — declare what you want, blade gw buildClientExtensionZip, deploy. In practice, hand-writing the yaml is one of the more frustrating parts of the Liferay dev loop:

  • 28 CX types, each with its own required and optional fields
  • No autocomplete on the yaml file — you're back to Ctrl-tabbing between VS Code and Liferay Learn to remember whether it's oAuthApplicationUserAgent or oAuthApplicationHeadlessServer
  • One typo and Blade explodes at build time with a message that half the time doesn't tell you which line
  • Object Actions, Workflow Actions, Notification Types each need their own Node.js microservice, Dockerfile, and paired OAuth app — always the same boilerplate
  • Site Initializers have a specific folder tree of ~10 subfolders with strict naming conventions
  • Multi-CX projects turn into a game of "which yaml am I editing again?"

Every one of those is a solvable problem, so we built CX Composer for Liferay — a VS Code extension that fills the tooling gap.


What CX Composer does

Six things, all inside VS Code:

1. Scaffold every CX type from a wizard. Right-click a folder → pick from all 28 types → get a working folder with client-extension.yaml, README.md, .gitignore, and any supporting code. Custom Element, IFrame, Global CSS/JS, Theme CSS, JS Import Map Entry, OAuth apps, Object Actions, Workflow Actions, Site Initializer, Commerce integrations — same flow.

2. IntelliSense on client-extension.yaml. Field completions and Markdown hover on every field of every type. The schema is pulled from Liferay's official YAML reference and verified against 131 real files in liferay/liferay-portal. Unknown fields flag with did-you-mean suggestions — instanceableqdid you mean instanceable?

3. Microservice scaffolding. For Object Action, Workflow Action, Notification Type, Object Validation Rule, Object Entry Manager, CAPTCHA, and every Commerce integration, choose "with code" → Node.js or TypeScript → get a full working starter: Express handler, package.json, two-stage Dockerfile, and — the piece nobody remembers — the paired oAuthApplicationUserAgent auto-declared in the same yaml with matching naming (<id>-oauth-agent). For Batch and Site Initializer, the paired OAuth is oAuthApplicationHeadlessServer (<id>-oauth-server).

4. Site Initializer folder tree. Pick "with code" on a Site Initializer and CX Composer generates the full deployable tree — site-configuration/site.json, layouts/01_home/page.json + page-definition.json, layout-page-templates/master-pages/main/, fragments scoped under fragments/group/, taxonomies, style books, object definitions, roles, notification templates. Every filename and field name is verified against Liferay's own liferay-aicontentwizard-site-initializer and site-initializer-masterclass samples.

5. Multi-Extension Workspace View. A sidebar tree lists every client-extension.yaml in your workspace with extension count, deploy status (built if a .zip in build/ is newer than the sources, stale if any source is newer, not built if no zip exists), and last-modified time. Click a row → jump to its yaml. Hover for one-click build or validate. Auto-refreshes on file or zip changes.

6. Per-extension build via Blade. Right-click any folder or any client-extension.yaml and select Build → CX Composer runs blade gw buildClientExtensionZip inside that folder. Monorepo-aware. No more "which yaml is this going to build?"

Plus a free layer of validators for client-extension.yaml, LCP.json, and Dockerfile — auto-run on save. Available on the Free tier for everyone.


Example: scaffolding an Object Action

Object Actions are one of the most common "wire in some custom logic" CX types. Historically you'd:

  1. Write the yaml block
  2. Register a separate oAuthApplicationUserAgent yaml block
  3. Cross-reference the two via oAuth2ApplicationExternalReferenceCode
  4. Set up a Node.js project by hand
  5. Write an Express handler
  6. Write a Dockerfile

With CX Composer, right-click a folder → Scaffold new project → pick Object Action → enter my-action → pick With codeTypeScript → enter resourcePath: /actions/my-action.

Result:

my-action/
├── client-extension.yaml     # my-action + my-action-oauth-agent
├── handler.ts                # Express, typed payload, mounted at resourcePath
├── package.json              # express, @types/express, tsx, typescript
├── tsconfig.json
├── Dockerfile                # two-stage: tsc build → node:20-alpine
├── .dockerignore
├── README.md
└── .gitignore
Enter fullscreen mode Exit fullscreen mode

The yaml:

my-action:
    type: objectAction
    name: my-action
    resourcePath: /actions/my-action
    oAuth2ApplicationExternalReferenceCode: my-action-oauth-agent

my-action-oauth-agent:
    type: oAuthApplicationUserAgent
    name: my-action-oauth-agent
    externalReferenceCode: my-action-oauth-agent
    homePageURL: https://liferay.com
    redirectURIs:
        - https://liferay.com
    scopes:
        - Liferay.Headless.Admin.User.everything
Enter fullscreen mode Exit fullscreen mode

And the handler:

import express, { Request, Response } from 'express';

const RESOURCE_PATH = '/actions/my-action';

const app = express();
app.use(express.json({ limit: '2mb' }));

app.get('/health', (_req, res) => res.json({ status: 'ok' }));

app.post(RESOURCE_PATH, async (req: Request, res: Response) => {
    const { objectActionRequest, objectEntry } = req.body ?? {};
    console.log('[handler]', RESOURCE_PATH, { objectActionRequest, objectEntry });

    // TODO: implement your logic

    res.json({ status: 'success' });
});

const port = Number(process.env.PORT ?? 8080);
app.listen(port, () => console.log(`[handler] on ${port} at ${RESOURCE_PATH}`));
Enter fullscreen mode Exit fullscreen mode

The Express route mounts on the exact resourcePath you entered. The paired OAuth app is already wired. npm install && npm run build && docker build . → you're ready to deploy.


How microservice CX types run at request time

The bigger picture: every user-agent CX type (Object Action, Workflow Action, Notification Type, Object Validation, Object Entry Manager, CAPTCHA, all Commerce integrations) walks the same runtime flowchart:

  1. User triggers action in the portal — clicks a custom Object Action button, completes a workflow transition, etc.
  2. Liferay routes to your microservice. It looks up the registered CX, resolves the URL, and POSTs the working object with the acting user's agent token attached as the Bearer.
  3. Your microservice validates the token. Invalid → return 401 and the cycle ends. Valid → you know exactly who triggered the action.
  4. Run business logic.
  5. Optional: call Liferay Headless.
    • As the user — reuse the incoming agent token on any /o/headless-*/v1.0/... endpoint. Liferay runs the call on the acting user's behalf; their permissions apply.
    • As the server — need higher scope than the user has? Do an OAuth client_credentials grant against a registered oAuthApplicationHeadlessServer, then call Headless with the resulting server token.
  6. Respond. Liferay applies the response to close the loop.

There's an animated version of this at cxcomposer.dev/how-microservices-work — three scenarios (invalid token / call Headless as user / escalate to server), pan and zoom, drag to rearrange.


Free vs Pro

  • Free — validation for client-extension.yaml, LCP.json, and Dockerfile, with auto-run on save, unknown-field diagnostics, and did-you-mean suggestions. Reference for all 28 types available at cxcomposer.dev/extensions.
  • Pro — $15/year — everything else: scaffolding, IntelliSense on client-extension.yaml, snippets, per-extension build, the workspace view, microservice code generation, Site Initializer folder tree, JS Import Map Entry with webpack.

Yearly Pro is deliberately priced under the "annoying to justify" line. If you're building even one CX per month, it pays for itself the first time you scaffold a working Object Action instead of copying one from a Slack thread.


Where to install

  • VS Code Marketplace: search for CX Composer for Liferay or install directly:
  code --install-extension CXComposer.cx-composer-for-liferay
Enter fullscreen mode Exit fullscreen mode

We're an independent third-party product — not affiliated with Liferay, Inc. Liferay® and Liferay DXP® are registered trademarks.

If you build for Liferay and give it a try, tell us what still hurts — every feature in the plugin so far started as someone's "why isn't this easier?"


Try it free at cxcomposer.dev. Feedback: hello@cxcomposer.dev.

Top comments (0)