Workflow Conversation State Management#
AdvancedChat (Chatflow) extends Dify's standard workflow engine with conversation-scoped state that persists across user turns. Each execution cycle maintains three categories of state:
| Layer | Scope | Persistence |
|---|---|---|
| System variables | Per-run | Ephemeral β rebuilt each turn |
| Environment variables | Per-workflow | Ephemeral β loaded from workflow definition |
| Conversation variables | Per-conversation | Persisted to DB β survive across turns |
All three are loaded into a VariablePool at execution start. The pool lives in GraphRuntimeState and is shared in-place across every node in the graph.
Key source files:
| File | Role |
|---|---|
system_variables.py | SystemVariableKey enum, build_system_variables(), build_bootstrap_variables(), preload helpers |
advanced_chat/app_runner.py | AdvancedChatAppRunner.run() β orchestrates all bootstrap steps |
node_factory.py | DifyNodeFactory β constructs nodes, injects memory using conversation_id |
conversation_variable_persist_layer.py | Event-driven layer that writes conversation variable mutations back to DB |
ConversationVariable model | DB model for persisted conversation state |
System Variables#
SystemVariableKey is a StrEnum defining all system variable names available in every workflow run:
conversation_idβ UUID of the current conversation (AdvancedChat only)queryβ current user message textfilesβ uploaded files for this turnuser_idβ resolved end-user identitydialogue_countβ number of completed turns (for multi-turn awareness)workflow_run_idβ unique ID for this executionapp_id,workflow_id,timestamp,invoke_fromβ execution context metadata
In AdvancedChatAppRunner.run(), system variables are assembled via build_system_variables() with conversation_id=self.conversation.id, binding the live conversation record to the execution from the start.
All system variables are stored under the SYSTEM_VARIABLE_NODE_ID sentinel namespace in the pool. Nodes access them via system_variable_selector(key) which returns (SYSTEM_VARIABLE_NODE_ID, key_name). The helper get_system_value() and get_system_text() wrap pool lookups for downstream consumers.
VariablePool Bootstrap#
On a fresh run, AdvancedChatAppRunner.run() builds the pool in three steps :
1. _initialize_conversation_variables() β load/create ConversationVariable rows from DB
2. build_bootstrap_variables(system, env, conversation) β flat list of Variable objects
3. VariablePool() + add_variables_to_pool() + add_node_inputs_to_pool()
build_bootstrap_variables() assembles variables from four namespaces, each keyed by a sentinel node ID constant:
| Namespace | Sentinel constant | Contents |
|---|---|---|
| System | SYSTEM_VARIABLE_NODE_ID | query, conversation_id, user_id, dialogue_count, etc. |
| 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 then written via add_node_inputs_to_pool() under the root node ID .
A GraphRuntimeState is constructed around the pool , and this state object is passed through to WorkflowEntry and all child engines β the pool is shared in-place, meaning downstream writes are immediately visible to all subsequent nodes.
On resume (HITL pause/resume): The pool is not rebuilt. The deserialized GraphRuntimeState carries its existing pool directly , and WorkflowEntry resumes from the paused position with all prior variable state intact.
Conversation Variables β Loading and Sync#
_initialize_conversation_variables() is called once per fresh run before the pool is built. It handles three cases:
- First turn β no
ConversationVariablerows exist for this(app_id, conversation_id)pair. All variables are created fromworkflow.conversation_variableswith their default values . - Subsequent turns β existing rows are loaded from the DB, carrying values mutated in prior runs .
- Variable additions post-conversation β if the workflow definition has added new variables after prior turns already ran,
_sync_missing_conversation_variables()creates the missing rows with default values and merges them with the loaded set.
The ConversationVariable DB model uses a composite primary key (id, conversation_id) and stores variable data as JSON in a data column. The to_variable() method deserializes via variable_factory.build_conversation_variable_from_mapping(), and from_variable() serializes via model_dump_json().
The resulting list of Variable objects is passed as conversation_variables to build_bootstrap_variables(), placing them under the CONVERSATION_VARIABLE_NODE_ID namespace in the pool.
Conversation Variables β Real-Time Persistence#
Conversation variable mutations during execution are persisted immediately via ConversationVariablePersistenceLayer , registered as a GraphEngineLayer on the graph engine :
conversation_variable_layer = ConversationVariablePersistenceLayer(
ConversationVariableUpdater(session_factory.get_session_maker())
)
workflow_entry.graph_engine.layer(conversation_variable_layer)
The layer listens for NodeRunVariableUpdatedEvent from the graphon engine . On each event it:
- Validates the selector has β₯ 2 elements
- Reads
conversation_idfrom the system variable namespace - Checks if the variable's selector prefix matches
CONVERSATION_VARIABLE_NODE_ID - If so, calls
ConversationVariableUpdater.update()to write to the DB
ConversationVariableUpdater.update() looks up the ConversationVariable row by (id, conversation_id), overwrites the data field with variable.model_dump_json(), and commits. Persistence is incremental and event-driven β each variable write during execution is immediately durable, not batched at run end. Variables outside the conversation namespace (e.g., environment variables) are silently ignored.
Multi-Turn Memory for LLM Nodes#
LLM, QUESTION_CLASSIFIER, and PARAMETER_EXTRACTOR nodes configured with a memory block need conversation_id available at construction time β before DifyNodeFactory.create_node() can finish building the node.
get_node_creation_preload_selectors() returns (SYSTEM_VARIABLE_NODE_ID, "conversation_id") for these node types when node_data.memory is set. preload_node_creation_variables() uses a VariableLoader to fetch any missing selectors from the DB and inserts them into the pool before node construction begins.
Inside DifyNodeFactory, _build_memory_for_llm_node() reads conversation_id from the pool via get_system_text(), fetches the Conversation ORM row, and constructs a TokenBufferMemory that includes prior message history in the LLM prompt.
Additionally, inject_default_system_variable_mappings() automatically adds sys.query to the LLM node's variable mapping when a memory config is present β ensuring the current user query is always wired in without requiring explicit configuration by the workflow author.