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 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 β The generator resolves the entry node (via
root_node_idor the default root) and checks its type. For Start entry nodes,_prepare_user_inputs()validates inputs against the app's variable schema, applies defaults, and handles file uploads. For webhook, schedule, and plugin entry nodes, the generator preserves the event inputs without modification, as they are already adapted event data - 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 |