Workflow Test Run Execution#
A workflow test run ("draft run") is the full-workflow execution triggered from the Dify console before a workflow is published. It differs from production invocations in three key ways: it operates on the draft Workflow record, sets InvokeFrom.DEBUGGER, and always streams its response.
API Entry Points#
Three console endpoints initiate draft runs (all defined in api/controllers/console/app/workflow.py):
| Endpoint | Class | App Mode |
|---|---|---|
POST /apps/<app_id>/workflows/draft/run | DraftWorkflowRunApi | WORKFLOW |
POST /apps/<app_id>/advanced-chat/workflows/draft/run | AdvancedChatDraftWorkflowRunApi | ADVANCED_CHAT |
POST /apps/<app_id>/workflows/draft/trigger/run | DraftWorkflowTriggerRunApi | WORKFLOW (trigger-based) |
All three parse their payload into a typed Pydantic model (DraftWorkflowRunPayload / AdvancedChatWorkflowRunPayload) and delegate to AppGenerateService.generate(..., invoke_from=InvokeFrom.DEBUGGER, streaming=True) .
Trigger-based runs additionally poll for an external trigger event, inject SKIP_PREPARE_USER_INPUTS_KEY = True to bypass normal input validation, and may supply a root_node_id to start execution from a specific trigger node .
Sub-node debug runs are also supported: single-node runs via DraftWorkflowNodeRunApi, and single-iteration/single-loop runs via WorkflowDraftRunIterationNodeApi and WorkflowDraftRunLoopNodeApi.
Workflow Resolution: Draft vs. Published#
AppGenerateService._get_workflow() is the single decision point :
invoke_from == InvokeFrom.DEBUGGERβWorkflowService.get_draft_workflow()- Any other
invoke_fromβWorkflowService.get_published_workflow()
If no draft exists, the call raises ValueError("Workflow not initialized").
App Generation and Entity Construction#
AppGenerateService._dispatch_generate() routes on app_model.mode . For debug runs (always streaming=True):
AppMode.WORKFLOWβWorkflowAppGenerator().generate(...)AppMode.ADVANCED_CHATβAdvancedChatAppGenerator().generate(...)
Inside WorkflowAppGenerator.generate(), the sequence is:
- File access scope β
_bind_file_access_scope()scopes all file lookups to the tenant - User input preparation β
_prepare_user_inputs()validates inputs against the app's variable schema; skipped whenSKIP_PREPARE_USER_INPUTS_KEYis set - Generate entity β a
WorkflowAppGenerateEntityis built withinvoke_from=DEBUGGER - Triggered-from tagging β
invoke_from == DEBUGGERmaps toWorkflowRunTriggeredFrom.DEBUGGING, stamped on the run record - Repository creation β fresh
WorkflowExecutionRepositoryandWorkflowNodeExecutionRepositoryinstances are created with a dedicatedsessionmaker
Worker Thread and Graph Initialization#
WorkflowAppGenerator._generate() spawns a threading.Thread that runs WorkflowAppRunner. The worker:
- Re-fetches the
Workflowrecord inside its own DB session (the main thread releases the connection before spawning) - Instantiates
WorkflowAppRunnerwith the generate entity, queue manager, and repositories - Calls
WorkflowAppRunner._init_graph(), which builds aDifyGraphInitContextand callsGraph.init()viaDifyNodeFactory
For full draft runs, graph_config is the complete workflow.graph_dict β every node and every edge. For single-iteration or single-loop debug runs (single_iteration_generate, single_loop_generate), the runner pre-filters graph_config inside _get_graph_and_variable_pool_for_single_node_run() to the target container node and its internal nodes, and passes skip_validation=True.
Graph Topology: Reachable vs. All Nodes#
WorkflowGraphTopology (api/core/workflow/graph_topology.py) provides static graph analysis. It is not invoked during execution β it is used at validation time (draft-save and publish).
from_graph(graph)β builds an incoming-edge adjacency map fromgraph_dictupstream_node_ids(target_node_id)β BFS returning all nodes reachable upstream of a target; edges referencing non-existent node IDs (half-deleted graphs) are silently skippedis_upstream(source, target)β point check used by Agent v2 validation
The draft vs. publish distinction in validation (WorkflowAgentNodeValidator):
validate_draft_workflow()βvalidate_previous_node_topology=False: all Agent v2 nodes are checked regardless of graph positionvalidate_published_workflow()βvalidate_previous_node_topology=True:previous_node_output_refsare confirmed against topologically reachable upstream nodes
During a draft test run, nodes that are unreachable from the start node (e.g., disconnected canvas nodes) exist in graph_dict but are never visited by the graph engine's traversal.
Key Files#
| File | Role |
|---|---|
api/controllers/console/app/workflow.py | HTTP endpoints for draft run, node run, trigger run |
api/services/app_generate_service.py | Draft vs. published workflow selection; generator routing |
api/core/app/apps/workflow/app_generator.py | Entity construction, DEBUGGING tagging, worker-thread dispatch |
api/core/workflow/graph_topology.py | Static graph analysis (upstream reachability, BFS) |
api/core/workflow/nodes/agent_v2/validators.py | Draft vs. published topology validation for Agent v2 nodes |