<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: CX Composer</title>
    <description>The latest articles on DEV Community by CX Composer (@cx_composer_dbdc1c511bfb4).</description>
    <link>https://dev.to/cx_composer_dbdc1c511bfb4</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4128365%2F837bf67d-7962-4ac7-9ed5-b1e5b11125c7.png</url>
      <title>DEV Community: CX Composer</title>
      <link>https://dev.to/cx_composer_dbdc1c511bfb4</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cx_composer_dbdc1c511bfb4"/>
    <language>en</language>
    <item>
      <title>Stop hand-writing Liferay Client Extensions</title>
      <dc:creator>CX Composer</dc:creator>
      <pubDate>Wed, 16 Sep 2026 16:02:03 +0000</pubDate>
      <link>https://dev.to/cx_composer_dbdc1c511bfb4/stop-hand-writing-liferay-client-extensions-4bom</link>
      <guid>https://dev.to/cx_composer_dbdc1c511bfb4/stop-hand-writing-liferay-client-extensions-4bom</guid>
      <description>&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;




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

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

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

&lt;p&gt;Every one of those is a solvable problem, so we built &lt;a href="https://cxcomposer.dev" rel="noopener noreferrer"&gt;CX Composer for Liferay&lt;/a&gt; — a VS Code extension that fills the tooling gap.&lt;/p&gt;




&lt;h2&gt;
  
  
  What CX Composer does
&lt;/h2&gt;

&lt;p&gt;Six things, all inside VS Code:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Scaffold every CX type from a wizard.&lt;/strong&gt; Right-click a folder → pick from all 28 types → get a working folder with &lt;code&gt;client-extension.yaml&lt;/code&gt;, &lt;code&gt;README.md&lt;/code&gt;, &lt;code&gt;.gitignore&lt;/code&gt;, 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. IntelliSense on &lt;code&gt;client-extension.yaml&lt;/code&gt;.&lt;/strong&gt; 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 &lt;code&gt;liferay/liferay-portal&lt;/code&gt;. Unknown fields flag with did-you-mean suggestions — &lt;code&gt;instanceableq&lt;/code&gt; → &lt;em&gt;did you mean &lt;code&gt;instanceable&lt;/code&gt;?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Microservice scaffolding.&lt;/strong&gt; 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, &lt;code&gt;package.json&lt;/code&gt;, two-stage &lt;code&gt;Dockerfile&lt;/code&gt;, and — the piece nobody remembers — the paired &lt;code&gt;oAuthApplicationUserAgent&lt;/code&gt; auto-declared in the same yaml with matching naming (&lt;code&gt;&amp;lt;id&amp;gt;-oauth-agent&lt;/code&gt;). For Batch and Site Initializer, the paired OAuth is &lt;code&gt;oAuthApplicationHeadlessServer&lt;/code&gt; (&lt;code&gt;&amp;lt;id&amp;gt;-oauth-server&lt;/code&gt;).&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;5. Multi-Extension Workspace View.&lt;/strong&gt; A sidebar tree lists every &lt;code&gt;client-extension.yaml&lt;/code&gt; in your workspace with extension count, deploy status (&lt;code&gt;built&lt;/code&gt; if a &lt;code&gt;.zip&lt;/code&gt; in &lt;code&gt;build/&lt;/code&gt; is newer than the sources, &lt;code&gt;stale&lt;/code&gt; if any source is newer, &lt;code&gt;not built&lt;/code&gt; 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.&lt;/p&gt;

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

&lt;p&gt;Plus a free layer of &lt;strong&gt;validators&lt;/strong&gt; for &lt;code&gt;client-extension.yaml&lt;/code&gt;, &lt;code&gt;LCP.json&lt;/code&gt;, and &lt;code&gt;Dockerfile&lt;/code&gt; — auto-run on save. Available on the Free tier for everyone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example: scaffolding an Object Action
&lt;/h2&gt;

&lt;p&gt;Object Actions are one of the most common "wire in some custom logic" CX types. Historically you'd:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write the yaml block&lt;/li&gt;
&lt;li&gt;Register a separate &lt;code&gt;oAuthApplicationUserAgent&lt;/code&gt; yaml block&lt;/li&gt;
&lt;li&gt;Cross-reference the two via &lt;code&gt;oAuth2ApplicationExternalReferenceCode&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set up a Node.js project by hand&lt;/li&gt;
&lt;li&gt;Write an Express handler&lt;/li&gt;
&lt;li&gt;Write a Dockerfile&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With CX Composer, right-click a folder → &lt;strong&gt;Scaffold new project&lt;/strong&gt; → pick &lt;strong&gt;Object Action&lt;/strong&gt; → enter &lt;code&gt;my-action&lt;/code&gt; → pick &lt;strong&gt;With code&lt;/strong&gt; → &lt;strong&gt;TypeScript&lt;/strong&gt; → enter &lt;code&gt;resourcePath: /actions/my-action&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The yaml:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;my-action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;objectAction&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-action&lt;/span&gt;
    &lt;span class="na"&gt;resourcePath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/actions/my-action&lt;/span&gt;
    &lt;span class="na"&gt;oAuth2ApplicationExternalReferenceCode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-action-oauth-agent&lt;/span&gt;

&lt;span class="na"&gt;my-action-oauth-agent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oAuthApplicationUserAgent&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-action-oauth-agent&lt;/span&gt;
    &lt;span class="na"&gt;externalReferenceCode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-action-oauth-agent&lt;/span&gt;
    &lt;span class="na"&gt;homePageURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://liferay.com&lt;/span&gt;
    &lt;span class="na"&gt;redirectURIs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;https://liferay.com&lt;/span&gt;
    &lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Liferay.Headless.Admin.User.everything&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RESOURCE_PATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/actions/my-action&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2mb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;RESOURCE_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;objectActionRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;objectEntry&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[handler]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RESOURCE_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;objectActionRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;objectEntry&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// TODO: implement your logic&lt;/span&gt;

    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[handler] on &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;RESOURCE_PATH&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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




&lt;h2&gt;
  
  
  How microservice CX types run at request time
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;User triggers action&lt;/strong&gt; in the portal — clicks a custom Object Action button, completes a workflow transition, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Liferay routes to your microservice.&lt;/strong&gt; It looks up the registered CX, resolves the URL, and POSTs the working object with the acting user's &lt;strong&gt;agent token&lt;/strong&gt; attached as the Bearer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your microservice validates the token.&lt;/strong&gt; Invalid → return &lt;code&gt;401&lt;/code&gt; and the cycle ends. Valid → you know exactly who triggered the action.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run business logic.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional: call Liferay Headless.&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;As the user&lt;/strong&gt; — reuse the incoming agent token on any &lt;code&gt;/o/headless-*/v1.0/...&lt;/code&gt; endpoint. Liferay runs the call on the acting user's behalf; their permissions apply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;As the server&lt;/strong&gt; — need higher scope than the user has? Do an OAuth &lt;code&gt;client_credentials&lt;/code&gt; grant against a registered &lt;code&gt;oAuthApplicationHeadlessServer&lt;/code&gt;, then call Headless with the resulting server token.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respond.&lt;/strong&gt; Liferay applies the response to close the loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's an animated version of this at &lt;a href="https://cxcomposer.dev/how-microservices-work" rel="noopener noreferrer"&gt;cxcomposer.dev/how-microservices-work&lt;/a&gt; — three scenarios (invalid token / call Headless as user / escalate to server), pan and zoom, drag to rearrange.&lt;/p&gt;




&lt;h2&gt;
  
  
  Free vs Pro
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free&lt;/strong&gt; — validation for &lt;code&gt;client-extension.yaml&lt;/code&gt;, &lt;code&gt;LCP.json&lt;/code&gt;, and &lt;code&gt;Dockerfile&lt;/code&gt;, with auto-run on save, unknown-field diagnostics, and did-you-mean suggestions. Reference for all 28 types available at &lt;a href="https://cxcomposer.dev/extensions" rel="noopener noreferrer"&gt;cxcomposer.dev/extensions&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pro — $15/year&lt;/strong&gt; — everything else: scaffolding, IntelliSense on &lt;code&gt;client-extension.yaml&lt;/code&gt;, snippets, per-extension build, the workspace view, microservice code generation, Site Initializer folder tree, JS Import Map Entry with webpack.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where to install
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VS Code Marketplace:&lt;/strong&gt; search for &lt;strong&gt;CX Composer for Liferay&lt;/strong&gt; or install directly:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  code &lt;span class="nt"&gt;--install-extension&lt;/span&gt; CXComposer.cx-composer-for-liferay
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docs and CX type reference:&lt;/strong&gt; &lt;a href="https://cxcomposer.dev" rel="noopener noreferrer"&gt;cxcomposer.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get Pro:&lt;/strong&gt; &lt;a href="https://store.cxcomposer.dev" rel="noopener noreferrer"&gt;store.cxcomposer.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We're an independent third-party product — not affiliated with Liferay, Inc. Liferay® and Liferay DXP® are registered trademarks.&lt;/p&gt;

&lt;p&gt;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?"&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Try it free at &lt;a href="https://cxcomposer.dev" rel="noopener noreferrer"&gt;cxcomposer.dev&lt;/a&gt;. Feedback: &lt;a href="mailto:hello@cxcomposer.dev"&gt;hello@cxcomposer.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>softwaredevelopment</category>
      <category>typescript</category>
      <category>vscode</category>
    </item>
  </channel>
</rss>
