Workflow Execution Persistence#
Dify uses a pluggable repository pattern to persist workflow execution state to the database. Two repository protocols β WorkflowExecutionRepository and WorkflowNodeExecutionRepository β are instantiated at runtime by DifyCoreRepositoryFactory, which reads a dotted module path from config and uses import_string to load the implementation class. This lets operators swap backends without code changes.
Pluggable Backends#
Two concrete implementations exist for each protocol.
SQLAlchemy (default) β SQLAlchemyWorkflowExecutionRepository writes synchronously inside a per-call session using session.merge() for upsert . An in-memory _execution_cache avoids redundant round-trips for hot records .
Celery-based (async) β CeleryWorkflowExecutionRepository serializes the WorkflowExecution domain model via model_dump() and calls save_workflow_execution_task.delay() β fire-and-forget . The background task runs on the workflow_storage Celery queue with max_retries=3 and exponential backoff (countdown = 60 * 2**retries) .
Config keys (set as dotted module path strings in env):
| Key | Default |
|---|---|
CORE_WORKFLOW_EXECUTION_REPOSITORY | ...SQLAlchemyWorkflowExecutionRepository |
CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY | ...SQLAlchemyWorkflowNodeExecutionRepository |
To switch to the async backend, set CORE_WORKFLOW_EXECUTION_REPOSITORY=core.repositories.celery_workflow_execution_repository.CeleryWorkflowExecutionRepository.
Celery Async Task: save_workflow_execution_task#
save_workflow_execution_task (in api/tasks/workflow_execution_tasks.py) is a @shared_task on the workflow_storage queue. It deserializes the payload, then either creates a new WorkflowRun DB record or updates an existing one β patching status, outputs, error, tokens, steps, and finished_at on updates . Failures retry up to 3 times with doubling delay.
HITL Pause State: Synchronous Path#
Pause persistence is intentionally not offloaded to Celery. When a GraphRunPausedEvent fires, PauseStatePersistenceLayer.on_event() runs synchronously in the execution thread to guarantee the pause record exists before the human-input form is surfaced to the user.
What gets packed into WorkflowResumptionContext:
serialized_graph_runtime_stateβ fullGraphRuntimeState.dumps()outputserialized_response_stream_filter_stateβResponseStreamFilter.dumps(); must be the exact same instanceWorkflowEntryis using β a different instance silently persists the wrong (empty) stategenerate_entityβ type-discriminated union ofWorkflowAppGenerateEntity|AdvancedChatAppGenerateEntity
The serialized JSON is written to object storage (not a DB column); the WorkflowPause DB record stores only the state_object_key . WorkflowRun.status is atomically set to PAUSED in the same transaction .
PauseStatePersistenceLayer is only injected when a PauseStateLayerConfig is passed to _generate() β it is absent in debug/single-iteration runs .
Resume Flow#
On resume, _resume_app_execution() in workflow_execute_task.py:
- Fetches
WorkflowPauseβ loads state bytes from object storage viapause_entity.get_state() - Deserializes via
WorkflowResumptionContext.loads() - Restores
GraphRuntimeStateandResponseStreamFilter - Dispatches to
_resume_workflow()or_resume_advanced_chat();stream=Trueis forced on the resumed generate entity
repo.resume_workflow_pause() atomically sets WorkflowPause.resumed_at and transitions WorkflowRun.status back to RUNNING .
For iteration/loop nodes containing Human Input nodes, CeleryWorkflowNodeExecutionRepository backfills its in-memory cache from the database on first access to a workflow_execution_id, so resumed workflows can see node executions saved in prior iterations .
API-Layer Repository (Service Reads)#
A separate repository (API_WORKFLOW_RUN_REPOSITORY, default DifyAPISQLAlchemyWorkflowRunRepository) handles service-layer reads: pagination, statistics, and pause CRUD. This is distinct from the core execution repository used during graph execution .
Key Source Files#
| File | Role |
|---|---|
api/core/repositories/factory.py | Protocols + DifyCoreRepositoryFactory; reads config, instantiates backend |
api/core/repositories/sqlalchemy_workflow_execution_repository.py | Synchronous SQLAlchemy backend with in-memory cache |
api/core/repositories/celery_workflow_execution_repository.py | Async Celery backend (fire-and-forget) |
api/tasks/workflow_execution_tasks.py | save_workflow_execution_task β workflow_storage queue |
api/core/app/layers/pause_state_persist_layer.py | PauseStatePersistenceLayer + WorkflowResumptionContext |
api/repositories/sqlalchemy_api_workflow_run_repository.py | API-layer repo: pause CRUD, statistics, cleanup |
api/configs/feature/__init__.py | RepositoryConfig β CORE_WORKFLOW_EXECUTION_REPOSITORY et al. |
api/core/workflow/nodes/human_input/callback.py | DifyHITLCallback β per-form persistence with execution_id-based deduplication |