8.2 Complex expressions & flow control with error handling
Key Takeaways
- Cloud flows run on the Workflow Definition Language (WDL), whose functions span string, collection, logical, conversion, date/time, and referencing categories.
- Compose stores a single computed value for reuse, variables support reassignment across a flow, and Parse JSON turns a raw payload into strongly typed dynamic content using a schema.
- Apply to each runs sequentially by default but supports Concurrency Control for parallel execution up to 50 threads.
- A Try/Catch/Finally pattern is built from Scope actions whose Configure run after settings determine whether Catch or Finally executes.
- The Terminate action explicitly sets a flow run's status to Failed, Cancelled, or Succeeded so a handled error isn't misreported as a plain success.
Cloud flows are built on the Workflow Definition Language (WDL), the JSON-based expression language underneath every visual designer canvas. Every value a flow computes dynamically — a concatenated string, a date offset, a value pulled from a prior step's output — is a WDL expression, and PL-400 candidates are expected to read and write them directly, not just click through the dynamic content picker.
Expression Categories
WDL functions fall into a handful of families:
| Category | Example functions | Typical use |
|---|---|---|
| String | concat(), replace(), split(), substring(), toUpper() | Building composite text, parsing delimited values |
| Collection | first(), last(), length(), union(), intersect() | Working with arrays from List rows or Parse JSON |
| Logical | equals(), and(), or(), not(), if() | Conditional expressions written inline in a field |
| Conversion | string(), int(), float(), json(), xml() | Casting between types, e.g. before math operations |
| Date/time | utcNow(), addDays(), formatDateTime() | Scheduling logic and date-column comparisons |
| Referencing | triggerBody(), triggerOutputs(), body('Action_name'), outputs('Action_name'), variables(), parameters() | Pulling values from the trigger, a named action, a flow variable, or a flow parameter |
Every expression starts with an @ prefix when embedded in raw JSON, though the visual designer hides that prefix inside its formula bar.
Compose, Variables, and Parse JSON
Three actions do a disproportionate amount of the work in complex flows:
- Compose stores a computed value (a concatenated string, an
if()result, a reshaped object) once so it can be referenced by name from later steps, instead of recomputing or re-copying the same expression repeatedly. It is a single assignment — cheaper than a variable when the value never needs to change. - Initialize variable / Set variable creates a named, mutable value that can be reassigned multiple times across the flow — the right choice for an accumulator inside a loop, where Compose's single-assignment model does not fit.
- Parse JSON takes a raw JSON payload — typically an HTTP response or an untyped Compose output — and, using a schema (hand-written or generated from a sample payload), turns its properties into strongly typed dynamic content that appears by name in the picker for every later step, instead of forcing manual
body('HTTP')?['field']expressions.
Looping and Branching
- Apply to each iterates an array (for example, the output of List rows). By default iterations run sequentially; Concurrency Control in the action's settings can be enabled to run iterations in parallel (up to 50 threads), trading strict ordering for throughput.
- Condition (If) and Switch branch the flow based on an expression or a matched value, each branch containing its own set of actions.
- Do until repeats a block until an expression evaluates true; it defaults to a cap of 60 iterations or a one-hour timeout, whichever comes first, both adjustable in the action's settings — a required safeguard against a runaway loop.
Error Handling with Scopes and Configure Run After
Cloud flows have no native try/catch keyword, so the standard pattern uses Scope containers combined with each action's Configure run after setting, which controls whether an action runs based on the outcome of the step(s) before it:
| Run-after option | Meaning |
|---|---|
| is successful | Runs only if the prior action succeeded (the default) |
| has failed | Runs only if the prior action failed |
| is skipped | Runs only if the prior action was skipped (e.g., its own run-after condition wasn't met) |
| has timed out | Runs only if the prior action exceeded its timeout |
A Try/Catch/Finally pattern groups the primary logic in a Try scope, adds a Catch scope configured to run after Try has failed, and adds a Finally scope with all four run-after boxes checked so it always executes regardless of outcome — mirroring cleanup code in traditional programming.
Terminate
Inside a Catch scope, the Terminate action explicitly ends the flow run with a chosen status — Failed, Cancelled, or Succeeded — plus an optional custom error code and message. Without it, a flow whose Catch scope handles an error gracefully can be misreported in run history as "Succeeded," hiding a real failure from monitoring; Terminate lets the developer set the status that actually reflects what happened.
A Practical Error-Handling Pattern
A production-grade Catch scope rarely calls Terminate alone. A typical sequence inside Catch is: (1) a Compose action that pulls the failed action's error details out of the Try scope's outputs using an expression such as result('Try_Scope'), which returns the status and error of every action inside that scope; (2) a notification action (an email or a Teams message to an operations channel) that surfaces the composed error details to a human; and (3) a Terminate action that ends the run as Failed with a message summarizing what went wrong. This turns an otherwise silent failure into an actionable alert instead of a run that quietly disappears into history.
Nested Scopes
Scopes can be nested inside one another, which is useful for isolating error handling around a specific sub-process without wrapping the entire flow in one enormous Try block. For example, a flow that processes several unrelated integrations can give each integration its own Try/Catch pair, so a failure in one integration is contained and reported without stopping the others — a design consideration the exam frames as "flow control actions including error handling" rather than a single global try/catch.
In a Try/Catch/Finally pattern built with Scope actions, how should the Catch scope's Configure run after setting be configured?
A Do Until loop has a logic error and its exit condition is never satisfied. What stops the flow from running indefinitely?