14 September 2026 · 9 min read
Camunda DXP — BPMN approvals for Liferay Objects
A Liferay DXP workspace for Camunda 8: creating a request Object starts a BPMN approval process, a reviewer decides in Tasklist, and the decision is written back to Liferay.
Camunda DXP is the Liferay side of a Camunda 8 setup: someone submits a request in Liferay, a BPMN process takes over, a reviewer approves or rejects it in Camunda Tasklist, and the decision lands back on the Liferay record — no polling, no copy-paste between two systems.
What it is
A Liferay Workspace for DXP 2026.Q3 with Camunda 8.3+ connectors. It contains a batch client extension for the Camunda Request object, a custom element widget to submit and track requests, a JAX-RS module that starts processes, and the BPMN process plus Camunda element templates. Source lives on GitHub: codepapu/camunda-dxp.
Why it exists
Liferay's built-in Kaleo workflow is fine for simple single-step reviews. When the business already models its approvals in BPMN — with Camunda Modeler, Tasklist, and operators watching processes in Operate — the portal should hand off to that engine instead of re-implementing the process a second time.
One process, one engine. The portal collects the request and shows the outcome; the workflow engine decides what happens in between.
Shape of the workspace
camunda-dxp
├── camunda/
│ ├── element-templates/ # Liferay inbound + outbound connectors
│ └── processes/ # request-approval.bpmn
├── client-extensions/
│ ├── camunda-dxp-batch/ # CamundaRequest object + statuses
│ └── camunda-dxp-custom-element/ # submit / status widget
├── modules/
│ └── camunda-dxp-rest/ # JAX-RS REST module + object action
└── docker-compose.camunda.yaml- Batch client extension — creates the
CamundaRequestobject, its status picklist, and an OAuth headless server app for Camunda to call back with. - Custom element —
<camunda-request-widget>lets a signed-in user submit a request and see its status. - REST module — the
camundaStartProcessobject action executor, a Camunda client, and a status callback endpoint.
The flow
- A user creates a Camunda Request — from the widget or the Objects UI.
- The
onAfterAddobject action runscamundaStartProcess. - The REST module starts the
request-approvalprocess and savesprocessInstanceKeywith statusinReview. - A reviewer completes the user task in Camunda Tasklist with
approvedorrejectedand an optional comment. - Camunda's HTTP JSON / Liferay Outbound Connector PATCHes
/o/c/camundarequests/{id}with the final status and comment.
The Camunda Request object
Company-scoped, with these fields:
title,description,requesterEmail— what the user submits.requestStatus— picklist:submitted→inReview→approved/rejected, orfailedif Camunda could not be reached.processInstanceKey,processDefinitionId— the link to the running process.decisionComment— the reviewer's note, or the error message on failure.
The BPMN process
request-approval.bpmn is deliberately small:
Request submitted
→ Mark in review on Liferay (HTTP PATCH requestStatus = inReview)
→ Approve or reject (user task: decision, comment)
→ Approved?
├─ approved → Mark approved on Liferay → Approved
└─ otherwise → Mark rejected on Liferay → RejectedEach “Mark … on Liferay” step is a service task using the io.camunda:http-json:1 connector against liferayBaseUrl + /o/c/camundarequests/{objectEntryId}. In Camunda Modeler you apply the Liferay Outbound Connector template to set the OAuth details.
REST and callbacks
GET /o/camunda-dxp/ready— health check.POST /o/camunda-dxp/v1.0/object-actions/start-process— start a process from an object payload.POST /o/camunda-dxp/v1.0/callbacks/status— Camunda writes status back, authenticated with anX-Camunda-Secretheader.
{
"objectEntryId": 123,
"requestStatus": "approved",
"decisionComment": "Looks good"
}Configuration
Settings live in Control Panel → System Settings (seeded from configs/local/osgi/configs): Camunda base URL and process-instances path, process definition id (default request-approval), auth type with token URL / client id / secret / audience for Camunda SaaS, the callback secret, and the Liferay base URL Camunda calls back to.
One switch matters more than the rest: fail-open. When it is on, a request still saves if Camunda is down — it is marked failed with the error in decisionComment. When it is off, the object action throws and the create fails.
Security notes
- Camunda calls Liferay with an OAuth headless server app scoped to the Camunda Request object — not an admin password.
- The status callback requires the shared
X-Camunda-Secret; change the defaultchange-mebefore anything leaves your laptop. - The local
portal-ext.propertiesenables BasicAuth and skips first-login for convenience — local development only.
Ship it
Prerequisites: JDK 21, Blade CLI, a DXP license in configs/local/deploy/, and Camunda 8.3+ (SaaS or the bundled Docker Compose file).
# Liferay
blade server init
blade server run
./gradlew deploy # then check http://localhost:8080/o/camunda-dxp/ready
# Camunda
docker compose -f docker-compose.camunda.yaml up- In Camunda Modeler, import
camunda/element-templates/*.json. - Open
request-approval.bpmnand apply the Liferay Outbound Connector to each service task (token URL/o/oauth2/token, client id / secret from thecamunda-dxp-batch-oahsapp). - Deploy the process with id
request-approval. - Add the Camunda Request widget to a page and submit a request — then approve it in Tasklist and watch the status change.
Takeaways
- Let Liferay own the record and Camunda own the process — each system does the job it is good at.
- An object action is the cleanest trigger: it fires on create and stores the process key on the entry.
- Write decisions back through the headless API with a narrowly scoped OAuth app.
- Decide up front whether a request may exist without a process — that is your fail-open setting.