Skip to main content

Process Orchestration Worked Examples

Non-Production Preview

Process Orchestration is currently available as a Non-Production Preview (NPP) feature, available to a limited number of customers. You should not process production data with NPP features. To request access, contact your Mambu Customer Success Manager. For more information, see Mambu Release Cycle — Feature Release Status.


This page covers four implementation patterns that together address the most common External Orchestration use cases. Each example shows the process flow, key BPMN patterns, and the complete process definition.

ExamplePattern
Currency rate syncSynchronous external API call with error handling
Async fire-and-forgetFire-and-forget with callback notification
KYC verification callbackInbound async callback from an external system
Human approval flowHuman task approval with loop and escalation

For background on BPMN concepts used here, see the BPMN Authoring Reference.


Currency rate sync

What it demonstrates: A straightforward two-step integration — call an external API, extract a value from the response, write it to Mambu. This is the canonical pattern for integrations that fetch external data and sync it into the Core Banking Engine.

Patterns covered:

  • Registering two destinations (external API + Mambu itself)
  • Extracting fields from a JSON response with assign-variable
  • Constructing a structured processResult with build-json
  • Catching an HTTP error from a Mambu call with a boundary event

Process flow

Prerequisites

  • Process Orchestration and MULTI_CURRENCY enabled on the tenant
  • EUR as an active currency; GBP as the base currency
  • Two Mambu user accounts (Maker and Checker)
  • Destinations frankfurter-api and mambu registered and in CURRENT status (see Getting Started)

BPMN

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:mbu="http://mambu.com/bpmn"
targetNamespace="process-orchestration">

<process id="fxRateSync" name="FX Rate Sync" isExecutable="true">

<startEvent id="start"/>

<!-- Fetch GBP/EUR rate. Produces fetchFxRate_response.rates.EUR -->
<serviceTask id="fetchFxRate" name="Fetch FX Rate" mbu:type="call-destination">
<extensionElements>
<mbu:destination>frankfurter-api</mbu:destination>
<mbu:method>GET</mbu:method>
<mbu:path>/v1/latest?from=GBP&amp;to=EUR</mbu:path>
</extensionElements>
</serviceTask>

<!-- Extract rate and effective date -->
<serviceTask id="extractRate" name="Extract Rate" mbu:type="assign-variable">
<extensionElements>
<mbu:variable name="gbpToEurRate" value="${fetchFxRate_response.rates.EUR}"/>
<mbu:variable name="startDate" value="${processInput.startDate}"/>
</extensionElements>
</serviceTask>

<!-- Write rate to Mambu. Currencies API requires Content-Type: application/json -->
<serviceTask id="updateMambuRate" name="Update EUR Rate" mbu:type="call-destination">
<extensionElements>
<mbu:destination>mambu</mbu:destination>
<mbu:method>POST</mbu:method>
<mbu:path>/api/currencies/EUR/rates</mbu:path>
<mbu:header name="Content-Type" value="application/json"/>
<mbu:header name="Accept" value="application/vnd.mambu.v2+json"/>
<mbu:payload>{"buyRate": ${gbpToEurRate}, "sellRate": ${gbpToEurRate}, "startDate": "${startDate}"}</mbu:payload>
</extensionElements>
</serviceTask>

<boundaryEvent id="updateMambuRateError" attachedToRef="updateMambuRate">
<errorEventDefinition/>
</boundaryEvent>

<serviceTask id="buildResult" name="Build Result" mbu:type="build-json">
<extensionElements>
<mbu:jsonTemplate>{"gbpToEurRate": "${r}", "mambuStatus": "${s}"}</mbu:jsonTemplate>
<mbu:jsonValues>{"r": "${gbpToEurRate}", "s": "${updateMambuRate_status}"}</mbu:jsonValues>
<mbu:resultVariable>processResult</mbu:resultVariable>
</extensionElements>
</serviceTask>

<serviceTask id="buildErrorResult" name="Build Error Result" mbu:type="build-json">
<extensionElements>
<mbu:jsonTemplate>{"outcome": "FAILED", "status": "${s}", "response": "${r}"}</mbu:jsonTemplate>
<mbu:jsonValues>{"s": "${updateMambuRate_status}", "r": "${updateMambuRate_response}"}</mbu:jsonValues>
<mbu:resultVariable>processResult</mbu:resultVariable>
</extensionElements>
</serviceTask>

<endEvent id="end"/>
<endEvent id="endError"/>

<sequenceFlow id="f1" sourceRef="start" targetRef="fetchFxRate"/>
<sequenceFlow id="f2" sourceRef="fetchFxRate" targetRef="extractRate"/>
<sequenceFlow id="f3" sourceRef="extractRate" targetRef="updateMambuRate"/>
<sequenceFlow id="f4" sourceRef="updateMambuRate" targetRef="buildResult"/>
<sequenceFlow id="f5" sourceRef="buildResult" targetRef="end"/>
<sequenceFlow id="f6" sourceRef="updateMambuRateError" targetRef="buildErrorResult"/>
<sequenceFlow id="f7" sourceRef="buildErrorResult" targetRef="endError"/>
</process>
</definitions>

Running

Start the process, passing the effective date for the rate:

curl -X POST "https://YOUR_HOST/api/processinstances/fxRateSync:start" \
-H "Content-Type: application/vnd.mambu.v2+json" \
-H "Accept: application/vnd.mambu.v2+json" \
-u "MAKER_USER:MAKER_PASSWORD" \
-d '{"startDate": "2026-07-14T00:00:00+00:00"}'

On success, the response body contains the rate and Mambu's HTTP status:

{"gbpToEurRate": 1.1584, "mambuStatus": 201}

Note: if Mambu returns DATE_ON_LAST_RATE_DATE, a rate for that date already exists. Use a different startDate or delete the existing rate first. The boundary event surfaces this as {"outcome": "FAILED", ...} rather than aborting the process.


Async fire-and-forget

What it demonstrates: A process that returns 204 No Content immediately to the caller and continues executing on the engine's background worker pool. When complete, it POSTs the result to a webhook URL the caller supplies. This pattern is appropriate for long-running workflows where the caller cannot wait for a synchronous response.

Patterns covered:

  • mbu:async="true" on <startEvent> for an immediate 204 response
  • JUEL arithmetic in an assign-variable step
  • Callback notification via a final call-destination step
  • Passing the callback URL at runtime in the start payload

Process flow

Prerequisites

  • Process Orchestration enabled on the tenant
  • Two Mambu user accounts (Maker and Checker)
  • A callback destination registered and in CURRENT status, pointing to your webhook receiver (for testing, webhook.site provides a free receiver with no setup)
  • The async executor activated on at least one cluster node

BPMN

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:mbu="http://mambu.com/bpmn"
targetNamespace="process-orchestration">

<process id="asyncAddDemo" name="Async Add Demo" isExecutable="true">

<!-- mbu:async="true": engine writes instance + variables + job atomically, returns 204 immediately -->
<startEvent id="start" mbu:async="true"/>

<!-- Compute sum and capture the callback path from the start payload -->
<serviceTask id="computeSum" name="Compute Sum" mbu:type="assign-variable">
<extensionElements>
<mbu:variable name="operand1" value="${processInput.operand1}"/>
<mbu:variable name="operand2" value="${processInput.operand2}"/>
<mbu:variable name="sum" value="${processInput.operand1 + processInput.operand2}"/>
<mbu:variable name="callbackPath" value="${processInput.callbackPath}"/>
</extensionElements>
</serviceTask>

<!-- POST result to the caller's webhook -->
<serviceTask id="notifyCaller" name="Notify Caller" mbu:type="call-destination">
<extensionElements>
<mbu:destination>callback</mbu:destination>
<mbu:method>POST</mbu:method>
<mbu:path>${callbackPath}</mbu:path>
<mbu:header name="Content-Type" value="application/json"/>
<mbu:payload>{"operand1": ${operand1}, "operand2": ${operand2}, "sum": ${sum}}</mbu:payload>
</extensionElements>
</serviceTask>

<endEvent id="end"/>

<sequenceFlow id="f1" sourceRef="start" targetRef="computeSum"/>
<sequenceFlow id="f2" sourceRef="computeSum" targetRef="notifyCaller"/>
<sequenceFlow id="f3" sourceRef="notifyCaller" targetRef="end"/>
</process>
</definitions>

Running

Start the process, passing operands and the webhook path:

curl -X POST "https://YOUR_HOST/api/processinstances/asyncAddDemo:start" \
-H "Content-Type: application/vnd.mambu.v2+json" \
-H "Accept: application/vnd.mambu.v2+json" \
-u "MAKER_USER:MAKER_PASSWORD" \
-d '{
"operand1": 3,
"operand2": 5,
"callbackPath": "/webhooks/your-receiver-id"
}'

The response is 204 No Content with processInstanceId in the response header. Within seconds, your webhook receiver receives:

{"operand1": 3, "operand2": 5, "sum": 8}

Poll GET /api/processinstances/{processInstanceId} until endTime is non-null to confirm completion.

Extension: add an error boundary to notifyCaller that POSTs to a different error endpoint, ensuring the caller is always notified regardless of whether the process succeeded or failed.


KYC verification callback

What it demonstrates: A process that makes an outbound call to a third-party system, then pauses at a message wait element until that system calls back with a result. This is the correct pattern for asynchronous vendor integrations — AML screening, credit bureau checks, identity verification — where the vendor's response can take seconds to hours.

Patterns covered:

  • Inbound callback correlation using <intermediateCatchEvent> with a message event
  • Auto-injected X-Callback-Execution-Id and X-Callback-Message headers on the outbound call
  • Delivering the callback via the Process Executions API
  • Branching on the callback result with an exclusive gateway

How callback correlation works

When a call-destination task includes an <mbu:callback message="..."/> declaration:

  1. The engine verifies at deploy time that the named message is reachable from that task in the same execution scope.
  2. At runtime, the engine automatically injects two headers onto the outbound HTTP request:
    • X-Callback-Execution-Id — the execution ID the vendor must include in their callback URL
    • X-Callback-Message — the message name to deliver
  3. The vendor calls POST /api/processexecutions/{executionId}/messages/{messageName}:deliver to resume the process.

The vendor never needs to know the process instance ID — only the execution ID and message name, both of which are delivered as request headers on the outbound call.

Process flow

Prerequisites

  • Process Orchestration enabled on the tenant
  • Two Mambu user accounts (Maker and Checker)
  • A kyc-vendor destination registered and in CURRENT status

BPMN

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:mbu="http://mambu.com/bpmn"
targetNamespace="process-orchestration">

<!-- Declare the callback message name -->
<message id="kycResultMessage" name="kyc_result"/>

<process id="kycVerificationDemo" name="KYC Verification Demo" isExecutable="true">

<startEvent id="start" mbu:async="true"/>

<!-- Send verification request to the KYC vendor.
The engine injects X-Callback-Execution-Id and X-Callback-Message automatically. -->
<serviceTask id="requestKycCheck" name="Request KYC Check" mbu:type="call-destination">
<extensionElements>
<mbu:destination>kyc-vendor</mbu:destination>
<mbu:method>POST</mbu:method>
<mbu:path>/verify</mbu:path>
<mbu:payload>{"clientId": "${processInput.clientId}", "documentType": "${processInput.documentType}"}</mbu:payload>
<mbu:callback message="kyc_result"/>
</extensionElements>
</serviceTask>

<!-- Park here until the vendor delivers the callback -->
<intermediateCatchEvent id="awaitKycResult" name="Await KYC Result">
<messageEventDefinition messageRef="kycResultMessage"/>
</intermediateCatchEvent>

<!-- Route on the vendor's decision -->
<exclusiveGateway id="kycDecision" default="rejectedFlow"/>

<endEvent id="approved" name="Approved"/>
<endEvent id="rejected" name="Rejected"/>

<sequenceFlow id="f1" sourceRef="start" targetRef="requestKycCheck"/>
<sequenceFlow id="f2" sourceRef="requestKycCheck" targetRef="awaitKycResult"/>
<sequenceFlow id="f3" sourceRef="awaitKycResult" targetRef="kycDecision"/>

<sequenceFlow id="approvedFlow" sourceRef="kycDecision" targetRef="approved">
<conditionExpression>${awaitKycResult_response.decision == 'APPROVED'}</conditionExpression>
</sequenceFlow>

<sequenceFlow id="rejectedFlow" sourceRef="kycDecision" targetRef="rejected"/>
</process>
</definitions>

Running

Start the process:

curl -X POST "https://YOUR_HOST/api/processinstances/kycVerificationDemo:start" \
-H "Content-Type: application/vnd.mambu.v2+json" \
-H "Accept: application/vnd.mambu.v2+json" \
-u "MAKER_USER:MAKER_PASSWORD" \
-d '{"clientId": "CLIENT-001", "documentType": "PASSPORT"}'

Response: 204 No Content. After the start, the engine sends the verification request to the KYC vendor. The outbound request includes:

X-Callback-Execution-Id: <execution-id>
X-Callback-Message: kyc_result

Vendor delivers the callback:

curl -X POST "https://YOUR_HOST/api/processexecutions/{executionId}/messages/kyc_result:deliver" \
-H "Content-Type: application/vnd.mambu.v2+json" \
-H "Accept: application/vnd.mambu.v2+json" \
-u "MAKER_USER:MAKER_PASSWORD" \
-d '{"decision": "APPROVED", "riskScore": 82}'

After delivery, the callback payload is available to downstream steps as:

VariableValue
awaitKycResult_response.decision"APPROVED"
awaitKycResult_response.riskScore82

Confirm completion:

curl -X GET "https://YOUR_HOST/api/processinstances/{processInstanceId}" \
-H "Accept: application/vnd.mambu.v2+json" \
-u "MAKER_USER:MAKER_PASSWORD"

Check endActivityId — it will be "approved" or "rejected" depending on the vendor's decision.

note

The requestKycCheck task and the awaitKycResult catch event must share the same execution token. A <subProcess> or <parallelGateway> split between them breaks correlation. This constraint is enforced at deploy time.


Human approval flow

What it demonstrates: A withdrawal process where a human approver must review and approve or reject the request via the Mambu Tasks inbox. The process loops on unclear responses (up to five attempts) and escalates to a supervisor if the primary approver cannot produce a clear decision.

Patterns covered:

  • <userTask> pausing execution for human input
  • Linking a task to an entity (deposit account) for a deep link in the Tasks inbox
  • Counter-based loop with an exclusive gateway
  • Supervisor escalation path
  • Reading task completion variables via {taskId}_{fieldName} pattern

Process flow

start
└─► bumpAttempt (increment attemptCount)
└─► approveWithdrawal (userTask → assignee gets CBE task)
└─► onApproverDecision (gateway)
├─► [APPROVED] commitWithdrawal (POST to Mambu) ──► end
├─► [REJECTED] buildRejectionResult ──► end
└─► [unclear] onLoopGuard (gateway)
├─► [attemptCount < 5] ──► bumpAttempt (loop)
└─► [5 strikes] ──► supervisorApproval (userTask)
└─► end

Prerequisites

  • Process Orchestration enabled on the tenant
  • Three Mambu user accounts: Maker, Approver/Checker, Supervisor
  • An active deposit account with sufficient balance
  • The mambu destination registered and in CURRENT status
  • Maker has permission to call POST /api/deposits/{id}/withdrawal-transactions

Key BPMN patterns

User task with task link and template:

<userTask id="approveWithdrawal"
name="Approve Counter Withdrawal"
mbu:assignedUser="${processInput.approverUsername}"
mbu:priority="50"
mbu:dueDate="P1D"
mbu:asyncLeave="true"
mbu:taskLinkType="DEPOSIT_ACCOUNT"
mbu:taskLinkId="${processInput.depositAccountId}"
mbu:template="WithdrawalApproval"/>

The mbu:taskLinkType + mbu:taskLinkId attributes render a deep link in the Tasks inbox. The approver can navigate directly to the deposit account screen — holder, balance, recent transactions — with one click.

Structured reply via Process Tasks API:

curl -X POST "https://YOUR_HOST/api/processtasks/{taskId}:complete" \
-H "Content-Type: application/vnd.mambu.v2+json" \
-H "Accept: application/vnd.mambu.v2+json" \
-u "APPROVER_USER:APPROVER_PASSWORD" \
-d '{"decision": "APPROVED", "comment": "ID verified at counter"}'

Each field in the reply body is stored as a process variable prefixed with the task id: approveWithdrawal_decision, approveWithdrawal_comment. The gateway conditions read them directly:

<exclusiveGateway id="onApproverDecision" default="unclearFlow"/>

<sequenceFlow sourceRef="onApproverDecision" targetRef="commitWithdrawal">
<conditionExpression>${approveWithdrawal_decision == 'APPROVED'}</conditionExpression>
</sequenceFlow>

<sequenceFlow sourceRef="onApproverDecision" targetRef="buildRejectionResult">
<conditionExpression>${approveWithdrawal_decision == 'REJECTED'}</conditionExpression>
</sequenceFlow>

<sequenceFlow id="unclearFlow" sourceRef="onApproverDecision" targetRef="onLoopGuard"/>

Loop counter:

<serviceTask id="bumpAttempt" name="Bump Attempt Counter" mbu:type="assign-variable">
<extensionElements>
<mbu:variable name="attemptCount"
value="${attemptCount == null ? 1 : attemptCount + 1}"/>
</extensionElements>
</serviceTask>

<exclusiveGateway id="onLoopGuard" default="escalateFlow"/>

<sequenceFlow sourceRef="onLoopGuard" targetRef="bumpAttempt">
<conditionExpression><![CDATA[${attemptCount < 5}]]></conditionExpression>
</sequenceFlow>

<sequenceFlow id="escalateFlow" sourceRef="onLoopGuard" targetRef="supervisorApproval"/>

Commit the withdrawal:

<serviceTask id="commitWithdrawal" name="Commit Withdrawal" mbu:type="call-destination">
<extensionElements>
<mbu:destination>mambu</mbu:destination>
<mbu:method>POST</mbu:method>
<mbu:path>/api/deposits/${processInput.depositAccountId}/withdrawal-transactions</mbu:path>
<mbu:header name="Content-Type" value="application/json"/>
<mbu:payload>{"amount": ${processInput.amount}}</mbu:payload>
</extensionElements>
</serviceTask>

Running

Start the process:

curl -X POST "https://YOUR_HOST/api/processinstances/withdrawalApproval:start" \
-H "Content-Type: application/vnd.mambu.v2+json" \
-H "Accept: application/vnd.mambu.v2+json" \
-u "MAKER_USER:MAKER_PASSWORD" \
-d '{
"depositAccountId": "YOUR_ACCOUNT_ID",
"amount": 500,
"approverUsername": "APPROVER_USERNAME",
"supervisorUsername": "SUPERVISOR_USERNAME"
}'

Response: 204 No Content. The process parks at the approveWithdrawal user task.

Find the open task:

curl -X GET "https://YOUR_HOST/api/processtasks?status=OPEN" \
-H "Accept: application/vnd.mambu.v2+json" \
-u "APPROVER_USER:APPROVER_PASSWORD"

Complete the task (happy path):

curl -X POST "https://YOUR_HOST/api/processtasks/{taskId}:complete" \
-H "Content-Type: application/vnd.mambu.v2+json" \
-H "Accept: application/vnd.mambu.v2+json" \
-u "APPROVER_USER:APPROVER_PASSWORD" \
-d '{"decision": "APPROVED", "comment": "ID verified, account in good standing"}'

Inspect the outcome:

curl -X GET "https://YOUR_HOST/api/processinstances/{processInstanceId}/history/variables" \
-H "Accept: application/vnd.mambu.v2+json" \
-u "MAKER_USER:MAKER_PASSWORD"

Walk-throughs

ScenarioSteps
Happy pathStart → Approver submits APPROVED → Withdrawal committed → COMPLETED
Loop with retriesStart → Approver submits unclear replies (up to 4) → Submits APPROVED on retry → Withdrawal committed
Full escalationStart → Approver submits 5 unclear replies → Supervisor task created → Supervisor submits APPROVED or REJECTED

Choosing a pattern

Use caseRecommended pattern
Fetch external data and write to MambuCurrency rate sync — synchronous, single response
Long-running workflow, no in-process waitAsync fire-and-forget — returns 204 immediately, notifies via webhook
Vendor integration with async callback (AML, KYC, credit check)KYC callback — parks until vendor delivers the result
Human approval requiredHuman approval flow — parks at a user task until assignee acts
Multiple independent external callsExtend async fire-and-forget with a parallel gateway — all branches read their own {taskId}_response after the join