codepapu
the playbook

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

text
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 CamundaRequest object, 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 camundaStartProcess object action executor, a Camunda client, and a status callback endpoint.

The flow

  1. A user creates a Camunda Request — from the widget or the Objects UI.
  2. The onAfterAdd object action runs camundaStartProcess.
  3. The REST module starts the request-approval process and saves processInstanceKey with status inReview.
  4. A reviewer completes the user task in Camunda Tasklist with approved or rejected and an optional comment.
  5. 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, or failed if 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:

text
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 → Rejected

Each “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 an X-Camunda-Secret header.
json
{
  "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 default change-me before anything leaves your laptop.
  • The local portal-ext.properties enables 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).

bash
# 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
  1. In Camunda Modeler, import camunda/element-templates/*.json.
  2. Open request-approval.bpmn and apply the Liferay Outbound Connector to each service task (token URL /o/oauth2/token, client id / secret from the camunda-dxp-batch-oahs app).
  3. Deploy the process with id request-approval.
  4. 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.