FinallyBlock finally-block
Define cleanup and audit logic that runs unconditionally after a TryBlock and CatchBlock — whether the try body succeeded or an exception was caught. Calls Memory.ExitTryBlock(), Memory.ExitVariableScope(), and clears the current exception from ExecutionMemory.
When to Use
- Release held resources: Release file locks, processing slots, reserved capacity, or external session handles that were acquired in the TryBlock — guaranteed regardless of whether the try body succeeded or failed.
- Update status records: Set a workflow status record from
"processing"to"completed"or"failed". Without FinallyBlock, failed workflows leave records perpetually stuck in"processing". - Write unconditional audit logs: Compliance frameworks require every action to be logged regardless of outcome. The FinallyBlock is the only reliable place for this — it fires even when CatchBlock is triggered.
- Clean up temporary files or data: Delete temp files or ephemeral records created during the TryBlock to prevent storage accumulation from failed workflow instances.
- Emit compliance audit events: Send audit events to a SIEM, event bus, or audit trail system unconditionally — regulatory requirements don't allow for "we skipped the audit event because the workflow failed."
Configuration
Execution Flow
Success Path (no exception)
- TryBlock executes
- All try-body nodes run normally
- CatchBlock is skipped
- FinallyBlock fires
- Workflow continues after finally
Exception Path
- TryBlock executes
- A node throws an exception
- CatchBlock fires, handles error
- FinallyBlock fires
- Workflow continues (or stops if Behavior=stop)
What FinallyBlock Does Internally
When FinallyBlock executes, the engine performs the following cleanup operations before firing the success port:
- Calls
Memory.ExitTryBlock()— pops the try/catch frame from the exception handler stack, re-enabling normal exception propagation for any exceptions thrown after the finally scope. - Calls
Memory.ExitVariableScope()— closes the variable scope opened by TryBlock. Variables that were scoped to the try/catch/finally block are removed from ExecutionMemory. - Clears
CurrentException— removes the current exception object from ExecutionMemory so downstream nodes (after finally) do not accidentally see a stale exception reference.
Output Ports
| Port | Description |
|---|---|
success | Fires after FinallyBlock completes its internal cleanup and all finally-scope nodes have executed. Downstream nodes connected here run as normal sequential workflow nodes — they are outside the try/catch/finally context. |
OutputData Fields
FinallyBlock itself writes no data fields. All variables set during the try and catch scopes that are still in-scope at finally time are accessible via $var. Exception variables (__exception_type__, __exception_message__, __exception_stacktrace__) are readable inside the finally scope if an exception was caught — they are cleared from memory after FinallyBlock completes.
Reading Exception State Inside FinallyBlock
Use exception variables inside the finally scope to write outcome-sensitive audit entries without duplicating the logging node:
| Expression | Use in FinallyBlock |
|---|---|
{{ $var.__exception_type__ !== undefined }} | Condition: was an exception caught? Use in an IfCondition to choose the outcome label for the audit log. |
{{ $var.__exception_message__ }} | Include the error message in the audit record when an exception occurred. |
Node Policies and GuardRails
- FinallyBlock must follow CatchBlock: The correct ordering is always TryBlock → [try body] → CatchBlock → [catch body] → FinallyBlock → [finally body]. FinallyBlock cannot appear before CatchBlock or be placed independently in the workflow.
- FinallyBlock is optional: Not every try/catch pattern needs a finally block. Add it only when you have genuine resource cleanup, unconditional logging, or status-update requirements.
- Keep finally scope minimal: FinallyBlock is for guaranteed cleanup, not for complex business logic. If finally-scope nodes fail, the engine handles that as a separate exception — avoid introducing new failure points in the finally scope.
- Exception variables are available inside finally scope: They are cleared after FinallyBlock completes, so access them within the finally-scope nodes, not in nodes that come after FinallyBlock in the workflow.
- Nodes after FinallyBlock are outside the error-handling context: Any exception thrown by a node after FinallyBlock's success port propagates normally to any outer TryBlock (or causes the workflow to fail if there is no outer try/catch).
Pattern Examples
Pattern 1 — Guaranteed Resource Release
A workflow acquires an exclusive processing slot before entering the TryBlock. The FinallyBlock always releases it, preventing slot starvation for other workflow instances.
VariableAssignment (acquiredSlot = true)
HttpRequest (POST /slots/acquire)
└─► TryBlock [key: "processTry"]
└─► success ──► [... processing nodes ...]
CatchBlock [key: "processCatch"]
Behavior: continue
└─► success ──► MongoDB (log error: {{ $var.__exception_message__ }})
FinallyBlock [key: "processFinally"]
└─► success ──► HttpRequest (POST /slots/release) (ALWAYS runs)
└─► [continue workflow]
Pattern 2 — Outcome-Sensitive Audit Log
Write a single audit log entry whose outcome field reflects success or failure without duplicating the logging node.
TryBlock [key: "auditTry"]
└─► success ──► [core business logic]
CatchBlock [key: "auditCatch"]
Behavior: continue
└─► success ──► VariableAssignment (outcomeStatus = 'failed')
FinallyBlock [key: "auditFinally"]
└─► success ──► IfCondition [key: "setOutcome"]
Condition: {{ $var.outcomeStatus !== 'failed' }}
├─► true ──► VariableAssignment (outcomeStatus = 'succeeded')
└─► false ──► (already set to 'failed' by catch)
[both paths merge to:]
└─► MongoDB (insert audit_log: {
workflowId: {{ $ctx.workflowId }},
outcome: {{ $var.outcomeStatus }},
ts: {{ $now }}
})
Pattern 3 — Complete Try/Catch/Finally for External Payment API
Full pattern showing all three nodes working together: TryBlock protects the API call, CatchBlock alerts the team on failure, FinallyBlock updates the payment status record unconditionally.
TryBlock [key: "payTry"]
└─► success ──► HttpRequest [key: "chargeApi"]
POST /payments/charge
{ amount: {{ $json.amount }}, token: {{ $json.paymentToken }} }
└─► VariableAssignment
paymentStatus = 'succeeded'
chargeId = {{ $output.chargeApi.chargeId }}
CatchBlock [key: "payCatch"]
Behavior: continue
└─► success ──► VariableAssignment (paymentStatus = 'failed')
└─► SlackNode (#payments-alerts)
"Payment failed for order {{ $json.orderId }}:
{{ $var.__exception_type__ }}
{{ $var.__exception_message__ }}"
FinallyBlock [key: "payFinally"]
└─► success ──► MongoDB (update orders SET
payment_status = {{ $var.paymentStatus }},
charge_id = {{ $var.chargeId }},
updated_at = {{ $now }}
WHERE id = {{ $json.orderId }})
└─► [workflow continues: route on $var.paymentStatus]
Sample Output
Success Port — Always Runs
{
"_finallyContext": {
"tryCaughtError": false,
"lastNodeOutput": {
"invoiceId": "INV-2025-00123",
"status": "processed",
"amount": 4500.00
},
"executionPath": "try_success",
"workflowId": "wf_invoice_processing",
"executionId": "exec_20250315_001"
}
}
Error Scenario — After CatchBlock
{
"_finallyContext": {
"tryCaughtError": true,
"caughtError": {
"errorCode": "VALIDATION_FAILED",
"message": "Invoice total mismatch",
"nodeName": "validateInvoice"
},
"executionPath": "catch_handled",
"workflowId": "wf_invoice_processing"
}
}
Expression Reference
| Expression | Description |
|---|---|
{{ $output.finallyNode._finallyContext.tryCaughtError }} | true if an error was caught in the CatchBlock; false if Try succeeded. |
{{ $output.finallyNode._finallyContext.executionPath }} | "try_success" or "catch_handled" — indicates which path was taken. |
{{ $output.finallyNode._finallyContext.caughtError.errorCode }} | Error code from the caught exception, if any. |