StopWorkflow stop
Terminate the current workflow execution with a final status. A terminal node — no downstream connections. The status is recorded in execution history and propagated to parent workflows when called from a SubWorkflow.
When to Use
- Explicit success gate: At the end of all branches in a complex workflow, use StopWorkflow with
status: "success"to mark clean completion. This ensures execution history always shows an explicit terminal status rather than an ambiguous implicit end, and triggers any downstream monitoring or alerting that watches for completed workflows. - Business rule rejection: After validating an order, if the product is out of stock or the customer account is suspended, use StopWorkflow with
status: "cancelled"and a descriptive message — e.g."Product SKU-4412 is out of stock. Order not processed."— to record the business reason without treating it as an error. - Error termination after CatchBlock: In a TryBlock/CatchBlock pattern, after exhausting retries or catching an unrecoverable error, call StopWorkflow with
status: "failed"and pass the exception message dynamically viamessage_expressionto preserve the root cause in execution history. - Conditional early exit: After processing a batch, check whether the result meets a threshold. Exit with
"success"if the threshold is met, or"cancelled"if no further action is needed — keeping execution history meaningful without forcing everything into a success/fail binary. - Sub-workflow result signaling: In a validation sub-workflow, return
status: "success"when data is valid orstatus: "failed"when it is not — the parent's SubWorkflow node then routes to itssuccessorerrorport accordingly, enabling clean branching in the parent without embedding the validation logic there.
Configuration
| Field | Required | Default | Description |
|---|---|---|---|
status |
Required | — | The final execution status to record. Must be one of "success", "cancelled", or "failed". This value is written to ExecutionStatusID in execution history and determines how a parent SubWorkflow node routes after this child workflow terminates. |
message |
Optional | — | A static human-readable reason for stopping. Recorded in execution history as stop_message. Visible in the BizFirst monitoring dashboard and execution detail views. If both message and message_expression are set, message_expression takes precedence. |
message_expression |
Optional | — | A BizFirst expression that evaluates to the stop message at runtime. Takes precedence over the static message field when both are present. Use this to include dynamic context — error details, entity IDs, or processing summaries — in the recorded message. |
message and message_expression are configured, the expression is evaluated first. If the expression resolves successfully, its result is used as the stop message. The static message value is only used if message_expression is absent or evaluates to null.
Status Semantics
| Status | SubWorkflow parent routes to | When to Use |
|---|---|---|
| success | parent's success port |
The workflow completed its intended purpose. All required steps executed without issue. Also correct for early exits where the intended outcome was already achieved (e.g. record already exists, no action needed). |
| cancelled | parent's error port (CHILD_WORKFLOW_CANCELLED) |
The workflow was intentionally stopped due to a business condition — not an error. Use for expected non-completions: approval rejected, product unavailable, customer already processed, user-initiated cancellation. The workflow did not fail; there was simply no useful work to complete. |
| failed | parent's error port (CHILD_WORKFLOW_FAILED) |
An unrecoverable condition was encountered and the workflow could not complete its purpose. Use for validation failures after retries, external API errors that cannot be resolved, data integrity violations, or any condition that requires human attention or escalation. |
Sample Configuration
Explicit success with a summary message
{
"status": "success",
"message_expression": "{{ 'Invoice ' + $output.createInvoice.invoiceId + ' generated and emailed to ' + $json.customerEmail }}"
}
Business rule cancellation (static message)
{
"status": "cancelled",
"message": "Customer already has an active subscription. No renewal action required."
}
Failed with dynamic exception detail
{
"status": "failed",
"message_expression": "{{ 'Validation failed: ' + $var.__exception_message__ }}"
}
Sub-workflow result signal (validation child)
{
"status": "failed",
"message_expression": "{{ 'Invalid customer record: ' + $output.validateFields.errors }}"
}
Validation Errors
StopWorkflow performs minimal validation — it is designed to always be reachable. There are no pre-execution configuration errors. The only runtime condition that prevents execution is if status is missing entirely from the node configuration, which will default to recording an empty status string.
| Condition | Behaviour |
|---|---|
status field is missing or empty | Workflow terminates but ExecutionStatusID is recorded as empty string. Monitoring dashboards may show an unknown status. Always provide a status value. |
message_expression evaluation fails | The expression error is silently swallowed and stop_message falls back to the static message value if present, or records an empty message. The workflow still terminates with the configured status. |
Output / Execution Ports
StopWorkflow has no output ports. It is a terminal node — no downstream connections are possible. When the workflow reaches a StopWorkflow node, execution ends for the current workflow context.
Execution History Fields Written
| Field | Type | Description |
|---|---|---|
ExecutionStatusID | string | The resolved status value: "success", "cancelled", or "failed". Written to the workflow execution record and used by parent SubWorkflow nodes for port routing. |
stop_message | string | The resolved message (from message_expression if set, otherwise message). Stored in execution history and displayed in the monitoring dashboard execution detail view. |
stopped_at | string (ISO-8601) | UTC timestamp of when StopWorkflow executed and the workflow terminated. |
stopped_by_node | string | The node key of this StopWorkflow node — identifies which node in the graph triggered termination, useful when a workflow has multiple StopWorkflow nodes on different branches. |
Sample Output
Execution history record written by StopWorkflow
{
"ExecutionStatusID": "failed",
"stop_message": "Validation failed: email field is not a valid email address",
"stopped_at": "2026-05-26T11:42:08Z",
"stopped_by_node": "stopOnValidationError"
}
Parent SubWorkflow error port — child stopped with "failed"
// $output.runValidation on parent's error port:
{
"error": {
"code": "CHILD_WORKFLOW_FAILED",
"message": "Validation failed: email field is not a valid email address"
}
}
Expression Reference
StopWorkflow is a terminal node and produces no output data. It writes only to execution history. The following expressions are available in the parent workflow on the error port after a child workflow terminates via StopWorkflow.
| Expression | Result (available in parent workflow) |
|---|---|
{{ $output.runValidation.error.message }} | The stop_message from the child's StopWorkflow node, surfaced on the parent's error port when status was "cancelled" or "failed". |
{{ $output.runValidation.error.code }} | Either CHILD_WORKFLOW_CANCELLED or CHILD_WORKFLOW_FAILED depending on the child's stop status. |
Node Policies & GuardRails
| Policy Area | Recommendation |
|---|---|
| Explicit terminal nodes | Always use StopWorkflow at the end of all main execution paths. Workflows that end by running out of connected nodes do not write an explicit terminal status — they complete with an ambiguous state. StopWorkflow ensures every execution path produces a clear, auditable outcome in history. |
| Descriptive messages | Always provide a message or message_expression. Stop messages appear in execution history and are the primary diagnostic tool when reviewing failed or cancelled workflow runs. A message like "Validation failed: email is invalid" is immediately actionable; an empty message requires re-running the workflow with debug logging to understand what happened. |
| Consistent status semantics across sub-workflows | Within a suite of related sub-workflows, establish a clear convention: "success" = intended outcome achieved, "cancelled" = graceful non-completion (expected business condition), "failed" = unexpected error requiring action. Inconsistent usage makes parent error port handling ambiguous and complicates monitoring dashboards. |
| Parent error port handling | When a sub-workflow uses StopWorkflow with "cancelled" or "failed", the parent SubWorkflow node routes to its error port. Always connect the parent's error port — leaving it unconnected causes the parent branch to terminate silently with no record of the child's stop reason. |
| cancelled vs failed distinction | Use "cancelled" for expected non-completions where the workflow made an intentional decision not to proceed — rejection by a business rule, user-initiated stop, item already processed. Use "failed" only for conditions that are unexpected, represent an error state, or require human investigation or alerting. Conflating the two pollutes failure metrics with expected business outcomes. |
| Placement after ParallelJoin | In workflows using ParallelFork/ParallelJoin, place StopWorkflow nodes only after the ParallelJoin node, not inside individual parallel branches. A StopWorkflow inside a parallel branch terminates all other branches immediately — if that is not the intended behaviour, use a VariableAssignment to record the outcome and let the ParallelJoin aggregate it instead. |
Examples
Example 1 — Validation Sub-Workflow Signaling Parent
A "Validate Order" child workflow checks product availability, customer credit, and shipping region eligibility. It uses StopWorkflow to signal the parent with a precise status and message. The parent SubWorkflow node routes to success or error accordingly, keeping all validation logic encapsulated in the child.
// Inside child workflow "Validate Order" (workflow ID: 55)
ManualTrigger [key: "start"]
└─► HttpRequest [key: "checkInventory"]
GET /inventory/{{ $json.productId }}
└─► IfCondition [key: "hasStock"]
Condition: {{ $output.checkInventory.quantityAvailable > 0 }}
├─► true ──► HttpRequest [key: "checkCredit"]
│ GET /customers/{{ $json.customerId }}/credit
│ └─► IfCondition
│ Condition: {{ $output.checkCredit.approved == true }}
│ ├─► true ──► StopWorkflow (status: "success", message: "Order validation passed")
│ └─► false ──► StopWorkflow (status: "failed", message: "Credit check failed for customer {{ $json.customerId }}")
└─► false ──► StopWorkflow (status: "cancelled", message: "Product {{ $json.productId }} is out of stock")
// Inside parent workflow:
SubWorkflow [key: "validateOrder"]
sub_workflow_id: 55
├─► success ──► [proceed to fulfillment]
└─► error ──► Slack (alert ops: {{ $output.validateOrder.error.message }})
Example 2 — CatchBlock Error Termination with Dynamic Message
A TryBlock wraps an external payment API call. If the call fails after retries, the CatchBlock catches the exception and routes to StopWorkflow with a message_expression that embeds the actual error detail from the exception variable into the recorded message.
TryBlock [key: "tryPayment"]
└─► body ──► HttpRequest [key: "chargeCard"]
POST /payments/charge
body: { cardToken: {{ $json.cardToken }}, amount: {{ $json.amount }} }
└─► catch ──► IfCondition [key: "isRetryable"]
Condition: {{ $output.chargeCard.statusCode == 429 || $output.chargeCard.statusCode == 503 }}
├─► true ──► Delay (duration_seconds: 30)
│ └─► success ──► [retry TryBlock — up to 3 attempts via Loop]
└─► false ──► StopWorkflow
status: "failed"
message_expression: "{{ 'Payment failed (non-retryable): HTTP ' + $output.chargeCard.statusCode + ' — ' + $var.__exception_message__ }}"
Example 3 — Multi-Branch Workflow with Explicit Success and Cancellation Exits
An order fulfilment workflow branches by order type. Each branch terminates with an explicit StopWorkflow node carrying a context-specific message. A shared "already fulfilled" early-exit path uses "cancelled" to distinguish intentional skipping from an actual error.
WebhookTrigger [key: "orderReceived"]
└─► HttpRequest [key: "checkDuplicate"]
GET /orders/{{ $json.orderId }}/status
└─► IfCondition [key: "isDuplicate"]
Condition: {{ $output.checkDuplicate.alreadyFulfilled == true }}
├─► true ──► StopWorkflow
│ status: "cancelled"
│ message_expression: "{{ 'Order ' + $json.orderId + ' already fulfilled on ' + $output.checkDuplicate.fulfilledAt + '. Skipped.' }}"
└─► false ──► Switch [key: "routeByType"]
├─► "digital" ──► HttpRequest (POST /delivery/digital ...)
│ └─► StopWorkflow
│ status: "success"
│ message_expression: "{{ 'Digital delivery sent for order ' + $json.orderId }}"
└─► "physical" ──► HttpRequest (POST /warehouse/pick ...)
└─► StopWorkflow
status: "success"
message_expression: "{{ 'Warehouse pick initiated for order ' + $json.orderId + ', tracking: ' + $output.warehousePick.trackingId }}"