Master the Mule Event’s three compartments, write DataWeave from scratch, control flow with Choice and Scatter-Gather, wrap operations in scopes, and build a reusable error envelope every API consumer will thank you for.
In Chapter 2 you built a database-driven CRUD API and got comfortable in Anypoint Studio. But if we’re honest, we were on autopilot β drag a connector, configure a field, run. Now it’s time to understand the engine underneath: events, transformations, routers, scopes, and structured errors. Nail these fundamentals and every advanced topic in this series becomes intuitive.
Every snippet in this chapter is part of the same runnable Mule 4.6 project we built in Ch.02. This chapter adds customers-api-v2.xml, the dwl/errorResponse.dwl module, and the Customer 360 Scatter-Gather endpoint.
By the end of this chapter you will be able to:
- Explain what a Mule Event is and name its three compartments with confidence
- Read and write payload, attributes, and variables β and know when to use each
- Write DataWeave from quick inline expressions to full reusable modules
- Control branching with Choice and parallel fan-out with Scatter-Gather
- Avoid the
payload[n].payloadgotcha that catches every junior - Wrap operations in Try, Async, and Until-Successful scopes
- Build a standardised error envelope pattern β write once, use everywhere
01 Β· The EnvelopeThe Mule Event β Three Compartments
Every time something triggers your flow β an HTTP request, a Scheduler tick, a file landing on FTP, a JMS message β Mule wraps everything into a Mule Event. Think of it as a courier envelope moving down a conveyor belt: every component opens it, reads it, can modify it, and passes it on.
The envelope has exactly three compartments. Understanding each one is the single most important thing you can learn in MuleSoft β every advanced topic builds on this.
1.1 Payload β The Business Data
The payload is your business data. It can be JSON, XML, a string, a binary file, a Java object, an array β whatever your flow is shuttling around. Most flow work is transforming the payload from one shape to another.
- The payload is mutable β every component can replace it entirely
- If a component produces no output (like a Logger), the payload passes through unchanged
- Critical pattern: before any DB operation, save the incoming payload to a variable β the DB connector overwrites payload with its result
An HTTP Listener receives POST /orders (payload = JSON order). A Set Variable stores the customer ID, a Transform Message reshapes the order for the DB schema, a DB Insert stores it. By the time DB Insert runs, the payload has been transformed twice. Without vars.originalOrder = payload at the start, you’d lose the original request body forever.
1.2 Attributes β The Envelope Stamp (Read-Only)
Attributes are metadata about how the message arrived. They’re automatically set by the source connector and you can’t modify them β they’re read-only.
| Source connector | Key attributes | Common access pattern |
|---|---|---|
| HTTP Listener | method, headers, queryParams, uriParams, requestPath |
attributes.queryParams.search |
| File / FTP | fileName, size, timestamp, directory |
attributes.fileName |
| JMS / AMQP | messageId, correlationId, redelivery, headers |
attributes.correlationId |
| Scheduler | frequency, timeUnit |
attributes.frequency |
1.3 Variables β Your Scratch Pad
Variables travel with the Mule Event from one component to the next β they live for the entire flow journey. Use set-variable for simple values and the target attribute on connectors to capture results without overwriting payload.
<!-- Save the incoming request payload before a DB call --> <set-variable variableName="requestBody" value="#[payload]"/> <!-- Target attribute: stash DB result in vars.customer, payload unchanged --> <db:select config-ref="H2_Config" target="customer"> <db:sql>SELECT * FROM customers WHERE id = :id</db:sql> <db:input-parameters>#[{ id: vars.customerId as Number }]</db:input-parameters> </db:select>
Connectors (DB, HTTP Request, File) all support a target attribute. When set, output goes to vars.targetName instead of overwriting payload. This lets you fire multiple data fetches and keep results in named variables β the cleanest approach for complex flows.
02 Β· AccessingThe Complete Access Reference
Here’s every access pattern you’ll use day-to-day. Bookmark this section β you’ll come back to it constantly.
// βββ PAYLOAD ββββββββββββββββββββββββββββββββββββββββββββββββββ payload // entire payload payload.customerId // field inside a JSON object payload[0] // first item of an array payload.name? // safe nav β null if missing, no error payload.email default "n/a" // fallback if null // βββ ATTRIBUTES βββββββββββββββββββββββββββββββββββββββββββββββ attributes.method // "GET", "POST", "PUT"β¦ attributes.queryParams.search // ?search=Alice attributes.uriParams.id // /customers/{id} attributes.headers.Authorization // "Bearer eyJβ¦" attributes.headers."Content-Type" // quotes for hyphenated keys // βββ VARIABLES ββββββββββββββββββββββββββββββββββββββββββββββββ vars.requestId // UUID set at flow start vars.startTime // timestamp for perf logging vars.customerData // DB result via target= // βββ USEFUL BUILT-INS βββββββββββββββββββββββββββββββββββββββββ now() // current DateTime uuid() // UUID v4 β perfect for trace IDs sizeOf(payload) // array length or string length isEmpty(payload) // true for empty array/string/null now() as String {format: "yyyy-MM-dd'T'HH:mm:ss'Z'"} // ISO 8601 attributes.headers['x-correlation-id'] default uuid() // trace pattern
03 Β· DataWeaveThe Engine of Transformation
Anywhere you see #[...] in a Mule config, DataWeave is evaluating at runtime. DataWeave is MuleSoft’s programming language for accessing and transforming data flowing through a Mule application β it’s functional, immutable, and built for streaming.
3.1 Inline Expressions β Quick Reference
| Expression | Returns | Real use case |
|---|---|---|
#[payload] |
Current payload | Pass entire body to Logger or HTTP Request |
#[payload.name?] |
Field or null | Safe access β no NullPointerException if absent |
#[payload.email default "n/a"] |
Value or fallback | Optional fields in CSV-to-JSON mappings |
#[sizeOf(payload)] |
Number | Log “found N records” after DB select |
#[isEmpty(payload)] |
Boolean | Choice condition β empty result = 404 |
#[uuid()] |
UUID string | Generate trace/request ID at flow start |
#[now()] |
DateTime | Timestamp for API response or DB insert |
3.2 Full Transformation Examples
%dw 2.0 output application/json --- payload map (row) -> { id: row.ID, name: row.NAME, email: row.EMAIL, createdAt: row.CREATED_AT as String {format: "yyyy-MM-dd"}, status: upper(row.STATUS default "active") }
%dw 2.0 output application/json var activeOnes = payload filter (row) -> row.ACTIVE == true --- { total: sizeOf(activeOnes), customers: activeOnes orderBy ((row) -> row.NAME) map (row) -> { id: row.ID, name: row.NAME, // conditional field β only if phone is not null (phone: row.PHONE) if (row.PHONE != null) } }
Variables declared in the header cannot be reassigned. Every transformation produces a new value β never modifies the input. This makes scripts safe to cache, test in isolation, and reason about. The %dw 2.0 directive at the top tells the runtime which language version to use.
04 Β· RoutingControlling Flow Logic
4.1 Choice Router β if / else if / else
The Choice router evaluates DataWeave conditions top-to-bottom and sends the event down the first matching branch. If nothing matches, the <otherwise> block runs. Exactly like an if/else chain in any language.
4.2 Scatter-Gather β Parallel Fan-Out
Scatter-Gather fires multiple routes simultaneously in separate threads and waits for all to complete. Use it when you need data from multiple independent sources and want maximum throughput.
A portal needs customer profile (CRM DB), last 10 orders (Order API), and account balance (Core Banking). Sequential = ~400ms. Parallel Scatter-Gather = ~140ms. That’s the difference between a fast app and a slow one at 100,000 req/day.
{ 0: msg0, 1: msg1, 2: msg2 }<scatter-gather timeout="5000" doc:name="Customer 360"> <!-- Route 0 β DB lookup --> <route> <db:select config-ref="H2_Config"> <db:sql>SELECT * FROM customers WHERE id = :id</db:sql> <db:input-parameters>#[{ id: vars.customerId }]</db:input-parameters> </db:select> </route> <!-- Route 1 β HTTP --> <route> <http:request config-ref="Order_API" path="/orders"/> </route> <!-- Route 2 β HTTP --> <route> <http:request config-ref="Banking_API" path="/balance"/> </route> </scatter-gather> <!-- Merge results β note the .payload accessor on each route --> <ee:transform> <ee:message> <ee:set-payload><![CDATA[%dw 2.0 output application/json --- { customer: payload['0'].payload[0], orders: payload['1'].payload, balance: payload['2'].payload }]]></ee:set-payload> </ee:message> </ee:transform>
(1) Always set timeout="5000" β without it a slow downstream blocks a thread forever. (2) Wrap each route in a <try> with on-error-continue so one failed route doesn’t kill the whole response. (3) Store any vars you need before entering β route variables are isolated from the main flow.
05 Β· ScopesWrapping Components with Special Behaviour
Scopes wrap groups of components and change how they behave as a unit β like decorators or block statements in code. The three you’ll use most:
Try Scope
Like a try-catch block. Attach typed error handlers for granular control.
- Catch
DB:CONNECTIVITY,HTTP:TIMEOUT, etc. - Return meaningful errors (503, 504)
- Use for any DB or HTTP call
- Combine with
on-error-continue
Async Scope
Runs in a background thread. Main flow doesn’t wait for completion.
- Fire-and-forget pattern
- Audit logs Β· analytics events
- Notifications Β· webhooks
- Never use for response-critical work
Auto-Retry
Retries the wrapped operation until success or maxRetries is reached.
- Backoff between attempts
- Flaky external APIs
- Eventually-consistent systems
- Final failure β typed error
<!-- Try with typed error handlers --> <try> <db:select config-ref="H2_Config"> <db:sql>SELECT * FROM customers WHERE id = :id</db:sql> <db:input-parameters>#[{ id: vars.customerId }]</db:input-parameters> </db:select> <error-handler> <on-error-continue type="DB:CONNECTIVITY"> <set-variable variableName="httpStatus" value="503"/> <logger level="ERROR" message="DB unavailable β returning 503"/> </on-error-continue> </error-handler> </try> <!-- Until-Successful: retry a flaky API up to 3 times, 2s between tries --> <until-successful maxRetries="3" millisBetweenRetries="2000"> <http:request config-ref="FlakyPartner_API" method="GET" path="/sync"/> </until-successful>
06 Β· ErrorsThe Reusable Error Envelope Pattern
Inconsistent error responses are the #1 complaint from API consumers. When every endpoint returns a different JSON shape on failure, consumers have to write custom parsing logic for each one. A standardised error envelope solves this permanently β write it once, use everywhere.
6.1 The DataWeave Error Builder β Write Once, Use Everywhere
Define this as a reusable module under src/main/resources/dwl/errorResponse.dwl. Now every flow can import and call buildError(β¦) with one line.
// src/main/resources/dwl/errorResponse.dwl // Import: import buildError, buildErrors from dwl::errorResponse %dw 2.0 fun buildError(statusCode, level, errorCode, description, cause = null, field = null) = { status: statusCode, level: level, timestamp: now() as String {format: "yyyy-MM-dd'T'HH:mm:ss'Z'"}, messages: [{ errorCode: errorCode, errorDescription: description, (cause: cause) if cause != null, (field: field) if field != null }] } fun buildErrors(statusCode, errors) = { status: statusCode, level: "ERROR", timestamp: now() as String {format: "yyyy-MM-dd'T'HH:mm:ss'Z'"}, messages: errors }
6.2 Every Error Scenario in One Place
%dw 2.0 output application/json import buildError, buildErrors from dwl::errorResponse --- // 400 β Invalid input format buildError(400, "ERROR", "INVALID_ID", "Customer ID must be a positive integer", "Received: '" ++ attributes.uriParams.id ++ "'", "uriParams.id") // 404 β Not found buildError(404, "ERROR", "CUSTOMER_NOT_FOUND", "No customer found with the given identifier", "ID '" ++ vars.customerId ++ "' not in customers table", "uriParams.id") // 422 β Multiple validation errors AT ONCE (don't just return the first!) buildErrors(422, [ { errorCode: "MISSING_EMAIL", errorDescription: "Email is required", field: "body.email" }, { errorCode: "INVALID_PHONE", errorDescription: "Invalid phone format", field: "body.phone" } ]) // 503 β Upstream service down buildError(503, "ERROR", "DB_UNAVAILABLE", "Service temporarily unavailable. Please retry shortly.", error.description) // 500 β Catch-all buildError(500, "ERROR", "INTERNAL_SERVER_ERROR", "An unexpected error occurred. Our team has been notified.")
6.3 Putting It All Together β Production-Grade Flow
<flow name="get-customer-v2-flow"> <http:listener config-ref="HTTP_Listener_config" path="/customers/{id}/v2" allowedMethods="GET"> <http:response statusCode="#[vars.httpStatus default 200]"/> </http:listener> <!-- Tracing β generate or propagate correlation id --> <set-variable variableName="requestId" value="#[attributes.headers['x-correlation-id'] default uuid()]"/> <set-variable variableName="startTime" value="#[now()]"/> <!-- Input validation before DB β no wasted queries --> <choice> <when expression="#[!(attributes.uriParams.id matches /^[1-9][0-9]*$/)]"> <set-variable variableName="httpStatus" value="400"/> <ee:transform> <ee:message> <ee:set-payload><![CDATA[%dw 2.0 output application/json import buildError from dwl::errorResponse --- buildError(400, "ERROR", "INVALID_ID", "Customer ID must be a positive integer", "Received: '" ++ attributes.uriParams.id ++ "'", "uriParams.id")]]></ee:set-payload> </ee:message> </ee:transform> </when> <otherwise> <!-- DB call wrapped in try with typed handler --> <try> <!-- β¦ db:select + choice for found/notfound β¦ --> <error-handler> <on-error-continue type="DB:CONNECTIVITY"> <!-- 503 envelope --> </on-error-continue> </error-handler> </try> </otherwise> </choice> <!-- Performance log on every request --> <logger level="INFO" message="#['[' ++ vars.requestId ++ '] ' ++ (vars.httpStatus default 200) as String ++ ' in ' ++ ((now() - vars.startTime) as String) ++ 'ms']"/> </flow>
Input validation before DB (no wasted queries) Β· typed error handlers per error category Β· structured errors for every failure case Β· request timing + trace ID in every log line Β· reusable error module used in three places. This is production-quality Mule development.
β Full source: github.com/nestaconnect/mulesoft-from-zero-to-hero
07 Β· RecapChapter Summary
The six fundamentals that turn every advanced topic intuitive:
Three compartments travel together
Payload (mutable business data) Β· attributes (read-only source metadata) Β· vars (your scratch pad). Output of A = input of B.
#[payload]β mutable#[attributes.x]β read-only#[vars.x]β mutable scratch- The chain is always forward
Functional, immutable transforms
Header (directives) + body (transform) separated by ---. Map, filter, reduce, orderBy.
- Inline
#[β¦]for quick access - Full scripts for complex transforms
- Conditional fields with
if - Import custom modules via
dwl::
if / else if / else routing
Evaluates conditions top-to-bottom. First match wins. otherwise is the required fallback.
- Input validation
- 200 / 404 / 400 branching
- Short-circuit on bad input
- Scales to any number of branches
Parallel fan-out Β· 3Γ faster
All routes execute concurrently. Mule waits for all to finish.
- Customer 360 Β· data merging
- β οΈ Access as
payload[n].payload - Always set
timeout - Wrap routes in
tryscopes
try Β· async Β· until-successful
Wrap groups of components with special behaviour β like decorators.
tryβ typed error handlingasyncβ fire-and-forgetuntil-successfulβ auto-retry- Wrap every DB + HTTP call
One envelope Β· every endpoint
status Β· level Β· timestamp Β· messages[]. Write the builder once, use everywhere.
buildError()Β· single messagebuildErrors()Β· bulk validation- Always return ALL errors at once
- Consistent shape = happy consumers
The runnable Mule project ships with customers-api-v2.xml (full error handling flow), dwl/errorResponse.dwl (the reusable module), and the Customer 360 Scatter-Gather endpoint. Clone, run, hit http://localhost:8081/customers/1/v2.