Workflow Fail Branch Architecture#
Overview#
The fail branch is a first-class workflow routing mechanism that lets a failing node redirect execution to a designated downstream "fail" edge instead of halting the entire run. In graphon's graph model, nodes that declare a fail-branch strategy are treated as exclusive branchers: only one outbound handle fires per execution β either the success path or the fail branch .
Dify's workflow engine delegates graph execution to the external graphon package (graphon==0.6.0) . Graphon owns the GraphEngine, variable pool, event routing, and built-in node implementations. Dify wires its own node logic (LLM, Agent, HTTP, etc.) into graphon's node registry via DifyNodeFactory, which acts as the integration point between the two layers.
The fail branch feature is most fully realized in the Workflow Agent Node v2 (DifyAgentNode), which implements per-output failure strategies including retries, default-value fallback, hard stop, and fail-branch routing.
Graphon Package and DifyNodeFactory#
graphon is Langgenius's open-source Python graph execution engine, pinned at graphon==0.6.0 in api/pyproject.toml. It provides:
- Queue-based
GraphEngineorchestration with event-driven execution - Graph parsing, validation, and shared
VariablePool/GraphRuntimeState - Built-in node implementations for common types (
start,end,llm,if-else,code,http-request,tool,iteration,loop,human-input, etc.) - Outbound edge routing, including exclusive branching for fail paths
Dify merges its own node implementations (core.workflow.nodes.*) with graphon's built-ins (graphon.nodes.*) through register_nodes(), then exposes the combined registry via DifyNodeFactory. For each node type, DifyNodeFactory.create_node() resolves the node class and injects Dify-specific dependencies (code executor, LLM model instance, HITL callbacks, output failure orchestrator, etc.) before handing the node back to graphon's engine .
The architectural boundary: graphon owns generic execution mechanics; Dify owns business logic (credentials, output validation, file management, HITL forms). This mirrors the HITL refactor that moved form schemas and timeout semantics from graphon back into Dify .
Per-Output Failure Strategy Configuration#
For the Agent Node v2, failure handling is configured per declared output rather than at the node level. The key models live in api/models/agent_config_entities.py:
OutputErrorStrategyβ three terminal strategies:STOP(fail the node hard),DEFAULT_VALUE(substitute a pre-configured value),FAIL_BRANCH(route through the fail branch edge)DeclaredOutputFailureStrategyβ holdsretryconfig (max retries, enabled flag),on_failure: OutputErrorStrategy, anddefault_valueDeclaredOutputConfigβ each declared output carries afailure_strategyfield defaulting to a populatedDeclaredOutputFailureStrategy
Because strategies are per-output, a single node run can have outputs with different strategies. The orchestrator merges them using a precedence rule: FAIL_BRANCH (rank 2) > STOP (rank 1) > DEFAULT_VALUE (rank 0) . If any failing output declares FAIL_BRANCH, that decision wins for the whole node β regardless of what other outputs declare.
DifyAgentNodeData itself is minimal (inherits BaseNodeData from graphon) and carries no node-level error strategy β all strategy is in the binding's declared_outputs .
OutputFailureOrchestrator β Decision Engine#
OutputFailureOrchestrator in api/core/workflow/nodes/agent_v2/output_failure_orchestrator.py is a stateless pure decision engine. The caller (agent_node._run) owns the retry counter and executes re-runs; the orchestrator only computes what to do next.
Four possible decisions :
| Decision | Action |
|---|---|
RETRY | Re-invoke the Agent backend (while retry budget remains) |
USE_DEFAULT | Substitute each failed output with its default_value |
FAIL_NODE | Halt node; no fail branch routing |
TAKE_FAIL_BRANCH | Emit failure event; graphon routes to fail branch edge |
Decision logic in decide():
- If any failing output still has retry budget (
current_attempt < max_retries), returnRETRY. - Once retry budget is exhausted, call
_merge_terminal_decisions()which picks the highest-precedenceOutputErrorStrategyacross all failures and maps it to a decision via_TERMINAL_STRATEGY_TO_DECISION.
The orchestrator is injected into DifyAgentNode by DifyNodeFactory._build_agent_node_init_kwargs() .
Fail Branch Trigger and Data Flow#
Triggering the Fail Branch#
Inside DifyAgentNode._run_inner(), after a successful Agent backend run, the type-check results are fed to the orchestrator . When the decision is TAKE_FAIL_BRANCH, the node sets:
error_type = "output_type_check_failed_fail_branch"
and yields a StreamCompletedEvent wrapping a NodeRunResult with status=FAILED . Graphon's engine sees this error_type and routes execution to the outbound fail branch edge.
Other failure paths (binding errors, backend errors, runtime exceptions) emit a _failure_event() with a different error_type string and do not trigger fail branch routing.
Data Available to Downstream Fail-Branch Nodes#
The _failure_event() method constructs the NodeRunResult with the following fields:
| Field | Content |
|---|---|
status | WorkflowNodeExecutionStatus.FAILED |
inputs | {"agent_backend_request": <redacted_request>} β captured on first attempt only |
process_data | {"agent_id": ..., "agent_config_snapshot_id": ..., "workflow_agent_binding_id": ...} |
metadata | {AGENT_LOG: {...}} β includes agent_backend status/run_id, output_failure_decision, output_failure_reason, output_type_check results, attempt count |
outputs | {} (empty on failure) |
error | Semicolon-separated "{name}[{kind}]: {reason}" strings from OutputFailureOrchestrator._summarize() |
error_type | "output_type_check_failed_fail_branch" |
ββββββββββββββββββββββββ TAKE_FAIL_BRANCH ββββββββββββββββββββββββ
β DifyAgentNode._run β βββ StreamCompletedEvent βββΆ Fail Branch Node β
β (type check fails) β error_type="β¦fail_branch"β reads inputs / β
β orchestrator.decide β β metadata / error β
ββββββββββββββββββββββββ ββββββββββββββββββββββββ
β RETRY / USE_DEFAULT / FAIL_NODE
βΌ
(handled inline, no branch routing)
Static Validation at Publish Time#
A non-blocking advisory validator (PR #37131, not yet merged at research date) performs static data-flow analysis at publish time and flags any variable reference that may not be populated on a concrete execution path. Nodes with error_strategy = FAIL_BRANCH are treated as exclusive branchers in this analysis β only one handle fires per run, so cross-branch variable references are flagged as warnings .