Variable Resolution and Template Substitution#
Overview#
Variable resolution in Dify workflows is built around the VariablePool (provided by the external graphon package). Every node reads from and writes to this single shared store during execution. Cross-node references use the {{#<node_id>.<variable_name>#}} template syntax, resolved at runtime by convert_template() from graphon.variables.template_resolution .
Key backend entry points:
| File | Purpose |
|---|---|
api/core/workflow/variable_pool_initializer.py | add_variables_to_pool(), add_node_inputs_to_pool() β pool bootstrap and node input registration |
api/core/workflow/system_variables.py | SystemVariableKey enum β defines system-level variable names |
api/core/workflow/node_factory.py | DifyNodeFactory.create_node() β wires each node type to its template resolution dependencies |
Key frontend entry points:
| File | Purpose |
|---|---|
web/.../nodes/_base/components/variable/utils.ts | getNodeUsedVars(), updateNodeVars(), toNodeOutputVars(), getVarType() |
web/.../workflow/hooks/use-workflow.ts | isVarUsedInNodes(), removeUsedVarInNodes() |
web/.../block-selector/snippets/use-insert-snippet.ts | Snippet insertion with ID remapping and variable selector rewriting |
VariablePool Lifecycle#
The pool lives in GraphRuntimeState and is bootstrapped once at workflow start . It is shared across all nodes and mutated in-place as each node executes.
Bootstrap phase β add_variables_to_pool() and add_node_inputs_to_pool() initialize the pool from four namespaces, each keyed by a sentinel node ID :
| Namespace | Contents |
|---|---|
System (SYSTEM_VARIABLE_NODE_ID) | query, files, conversation_id, user_id, workflow_run_id, etc. β defined in SystemVariableKey enum |
Environment (ENVIRONMENT_VARIABLE_NODE_ID) | Workflow-level env vars |
Conversation (CONVERSATION_VARIABLE_NODE_ID) | Persisted conversation state |
RAG pipeline (RAG_PIPELINE_VARIABLE_NODE_ID) | Knowledge base pipeline inputs |
User inputs from the START node are written via add_node_inputs_to_pool(), which stores each input under the primary node ID and any registered aliases β enabling snippet workflows to expose inputs under both __snippet_virtual_start__ and the alias start .
Execution phase β As each node completes, it writes its outputs back with variable_pool.add((node_id, key), value). Downstream nodes resolve those values when their templates are processed. LLM-compatible nodes that use memory require CONVERSATION_ID to be present at construction time; get_node_creation_preload_selectors() handles pre-loading those selectors before DifyNodeFactory.create_node() runs .
Population failures are a common root cause of runtime errors β see Common Failure Modes.
Template Syntax: {{#...#}}#
The placeholder format is {{#<node_id>.<variable_name>#}}. The template engine uses a regex that only accepts alphanumeric characters and underscores in both segments :
\{\{#([a-zA-Z0-9_]{1,50}(?:\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10})#\}\}
β οΈ Silent failure: Node IDs containing hyphens (e.g.,
{{#my-node.output#}}) will not match this regex. The placeholder passes through to the output as raw text with no error raised.
Variables are stored and retrieved via selector tuples: (node_id, variable_name). convert_template() returns an object with three output properties :
.textβ plain text rendering.logβ log-friendly rendering (used for model input logging).markdownβ markdown rendering (used for Human Input form content)
Special namespaces β On the frontend, selectors beginning with sys, env, conversation, or rag are treated as special variables and mapped to their sentinel node IDs . System variables like sys.query and sys.files appear under the Start node's output in the variable picker.
Template Substitution by Node Type#
DifyNodeFactory.create_node() wires each node type to its template-resolution dependencies . All nodes receive graph_runtime_state (containing the pool); higher-level rendering helpers vary:
| Node Type | Template Rendering | Notes |
|---|---|---|
| LLM | jinja2_template_renderer + retriever_attachment_loader | Jinja2 for prompt templates; retriever loader for KB context |
| Template Transform | jinja2_template_renderer | Pure Jinja2 output |
| HTTP Request | graphon Executor with variable_pool | URL, headers, params, and body all substituted; see JSON details below |
| Human Input | convert_template() β .markdown | Stored as rendered_content in DB at pause time |
| Question Classifier / Parameter Extractor | Direct pool access | No jinja2_template_renderer |
| Code | Variables injected as named arguments | Not template-rendered |
| Answer / End | replaceOldVarInText() on text fields | Inline {{#...#}} replacement |
| Agent | runtime_support.py | Inputs and tool parameters resolved from pool at runtime |
HTTP Request Node: JSON Body Escaping#
Body type handling differs by content type :
form-data/x-www-form-urlencoded:convert_template()is called; substituted values become strings.json: Usesconvert_template_escape(), which additionally escapes"β\"and\nβ\\nin string values before callingjson.loads(). Non-string segments (numbers, objects) are left unescaped.
Type-aware substitution in JSON bodies:
- Numbers remain unquoted:
{"count": {{#node.count#}}}β{"count": 42} - Strings appearing unquoted in the template are auto-quoted via
json_repair(v0.60.1) as a fallback - Objects/dicts expand inline
The json_repair fallback fixes structurally broken JSON that results from substitution. Unit tests covering these cases (including UUID string preservation) live at api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py .
Frontend: Variable Discovery and Selector Rewriting#
All frontend variable management flows through utils.ts and use-workflow.ts.
getNodeUsedVars(node) β Returns all ValueSelector[] arrays referenced by a node. Covers all node types, including {{#...#}} patterns in text fields (matchNotSystemVars) and explicit value_selector fields in structured configs. This is the authoritative source of truth for what variables a node depends on.
updateNodeVars(oldNode, oldSelector, newSelector) β Rewrites all references from oldSelector to newSelector across every field of a node using Immer's produce. Called when an upstream node is renamed, deleted, or when snippets are inserted.
toNodeOutputVars() β Builds the available variable list for the picker UI. Adds synthetic virtual nodes for Environment (env), Conversation (conversation), System (global), and RAG pipeline (rag) variables alongside regular nodes.
getVarType() β Resolves the runtime type of a ValueSelector by traversing the Var tree of node output vars. Handles structured output schemas, file sub-variables, iteration/loop item types, and special namespace prefixes.
isVarUsedInNodes (in use-workflow.ts) β Checks if a variable selector is referenced in any downstream node in the workflow branch. Used to gate deletion confirmation dialogs.
removeUsedVarInNodes (in use-workflow.ts) β Removes variable references from downstream nodes when a variable is deleted. Calls updateNodeVars with an empty selector [] for each affected node.
Snippet Insertion: ID Remapping#
When a snippet is inserted via useInsertSnippet, every node gets a fresh ID to avoid collisions. The remapping is orchestrated by remapSnippetGraph :
remapSnippetNodeStructuralReferencesβ rewritesiteration_id,loop_id,start_node_id, andoutput_selectorwithin Iteration/Loop container nodes.remapSnippetNodeVariableReferencesβ callsgetNodeUsedVars()to find allValueSelectorarrays, then callsupdateNodeVars()for any selector whose leading node ID maps to a new ID. Ensures intra-snippet cross-node references remain valid. (Added in PR #39843.)remapSnippetAssignerReferencesβ handlesBlockEnum.Assignernodes explicitly (not covered by the generic traversal):- v2 assigners: remaps
items[].variable_selector(target) anditems[].variable_inputwheninput_type: 'variable' - Legacy v1 assigners: remaps
assigned_variable_selector(target) andinput_variable_selector(source) - External namespace selectors (
['env', 'api_key'],['conversation', 'latest']) are preserved unchanged
- v2 assigners: remaps
- Edge
source/targetfields and_children[].nodeIdmetadata are also rewritten from the ID map.
Regression history: Prior to PR #39843, variable selectors inside node data configs were not updated during insertion β stale node IDs were silently retained, causing wrong or missing pool lookups at runtime .
Variable Assigner vs. Variable Aggregator#
Two similarly named node types serve distinct roles :
-
BlockEnum.VariableAssigner(labeled "Assigner" in the UI; types inassigner/types.ts) β Writes a value into a conversation variable, environment variable, or other mutable store. Has v1 (legacy) and v2 schemas;BlockEnum.Assigneris the current form,BlockEnum.VariableAssigneris retained for backward compatibility . -
BlockEnum.VariableAggregator(legacy alias:VariableAssigner) β Merges variable outputs from multiple upstream branches (e.g., after an If-Else) into a single resolvedoutputvariable. Silently ignores skipped branches, making it the recommended workaround for "Variable Not Found" errors on branch convergence .
Both node types share the same frontend getNodeUsedVars / updateNodeVars handling path and share the VariableAssignerNodeType data model .
Common Failure Modes#
LLM node receives model config instead of template output
Symptom: upstream Template node renders correctly but the LLM receives a config object. Root cause: the VariablePool does not yet contain the upstream node's output when the LLM node runs. Check that the Template node is connected by an edge and look for parallel-branch race conditions.
Human Input node shows raw {{#...#}} after page refresh
Symptom: after a page refresh during a pending HITL pause, the form shows raw placeholders like {{#1778488854176.text#}}. Root cause: either render_form_content_before_submission() failed to call convert_template() at pause time (pool not yet populated), or the API returns the raw form_content field instead of rendered_content. The frontend does not resolve variables client-side.
"Variable Not Found" on branch convergence
Symptom: a skipped conditional branch leaves its output variables undefined; downstream nodes freeze with "Variable Not Found." Workaround: wrap each variable in a Variable Aggregator node β it ignores skipped branches and always provides a resolved output.
Silent substitution failure for hyphenated node IDs
The {{#...#}} regex only allows [a-zA-Z0-9_] in the node ID segment. References to nodes with hyphens pass through as raw text with no error .
Empty variable selector fails backend validation
Clearing an optional file input in a tool or agent node can save an empty value_selector: [] to the node config. The backend rejects this during validation. Fix: delete the parameter row entirely rather than clearing the value.