Human Input Node (HITL)#
The Human Input node (HITL β Human-in-the-Loop) pauses a Dify workflow or chatflow at a designated point, presents an interactive form to an operator, and resumes execution based on the operator's response. It works in both Chatflow and Workflow app types and can be used inside Loop and Iteration container nodes .
The form supports configurable field types (Paragraph, Select, File, FileList), user action buttons that route execution to different branches, delivery via WebApp or Email, and node-level and global timeouts. It was introduced in PR #32060 .
Backend Implementation#
Core Node Module#
All node-specific logic lives in api/core/workflow/nodes/human_input/ :
| File | Purpose |
|---|---|
entities.py | Form schema: HumanInputNodeData, FormInputConfig subtypes, FormDefinition, validate_human_input_submission() |
callback.py | DifyHITLCallback β bridges Dify form semantics to graphon HITL decisions; handles form creation, timeout judgment, and placeholder rendering |
enums.py | FormInputType, HumanInputFormStatus (WAITING / EXPIRED / SUBMITTED / TIMEOUT), ButtonStyle, TimeoutUnit |
pause_reason.py | HumanInputRequired pause reason + DifyHITLEventType |
boundary.py | enrich_graph_pause_reasons() β translates graphon session IDs to Dify form IDs before DB persistence |
session_binding.py | SessionBinding β maps between graphon session IDs and Dify form IDs |
Architecture boundary: graphon owns generic pause/resume infrastructure (HITLContext, HITLDecision, PauseRequested); Dify owns workflow-specific form schema, field types, timeout semantics, and DifyHITLCallback .
Timeout Semantics#
Two distinct layers :
- Node-level (
TIMEOUT): returnsExpired(selected_handle="__timeout"), routing to the timeout branch. - Global expiry (
EXPIREDorWAITING+ global deadline passed): treated as invalid, raisesAssertionError. Global deadline =created_at + HUMAN_INPUT_GLOBAL_TIMEOUT_SECONDS.
Service Layer#
api/services/human_input_service.py exposes :
get_form_definition_by_token()/get_form_definition_by_token_for_console()β form retrieval for WebApp vs. console recipientssubmit_form_by_token()β validates, marks submitted, enqueues resumeenqueue_resume()β dispatchesresume_app_executionCelery task onworkflow_based_app_executionqueue
Variable Template Rendering#
DifyHITLCallback.render_form_content_before_submission() resolves {{#...#}} placeholders via convert_template() before pause. The frontend relies entirely on the pre-rendered rendered_content stored in the DB β it does not resolve variables client-side .
Frontend Implementation#
Component Hierarchy#
Forms appear in two rendering contexts that share inner components :
HumanInputFormList β active (unsubmitted) forms
ββ ContentWrapper β card with node icon + expand toggle
ββ UnsubmittedHumanInputContent
HumanInputFilledFormList β completed (submitted) forms
ββ ContentWrapper (expanded=true)
ββ SubmittedHumanInputContent
The same ContentWrapper is used in both the chat answer path (web/app/components/base/chat/chat/answer/) and the workflow panel path (web/app/components/workflow/panel/).
Key Frontend Files#
| Path | Role |
|---|---|
β¦/answer/human-input-form-list.tsx | Chat-path active forms; display_in_ui filtering; delivery method tips |
β¦/answer/human-input-filled-form-list.tsx | Chat-path submitted forms |
β¦/panel/human-input-form-list.tsx | Panel-path active forms; reads delivery config from ReactFlow store |
β¦/panel/human-input-filled-form-list.tsx | Panel-path submitted forms |
web/app/components/workflow/nodes/human-input/panel.tsx | Node configuration panel (fields, actions, delivery, timeouts) |
web/app/components/base/chat/chat/hooks.ts | useChat β manages isResponding, human input form state, and workflow event callbacks |
isResponding and Chat Input State#
useChat in hooks.ts drives the chat input's enabled/disabled state through isResponding :
onWorkflowStartedβ setsisResponding = trueonWorkflowPausedβ setsisResponding = falseonHumanInputFormFilledβ setsisResponding = truewhen form is submitted and execution resumes
Both HumanInputFormList variants filter to display_in_ui === true before rendering . Per-node showEmailTip, isEmailDebugMode, and showDebugModeTip flags are computed from delivery_methods config to show contextual hints to the operator .
Known Issues and Version-Specific Fixes#
1. No reply after Conditional Branch (β€1.15.0) β Fixed in PR #38540#
Symptom: After a Human Input pause in a chatflow with an upstream conditional branch, the downstream Answer node ran successfully but no message appeared in the chat UI .
Root cause: On resume, iter_dify_graph_engine_events() instantiated a fresh ResponseStreamFilter with no record of edges traversed before the pause. The Answer node's paths_map entry never emptied, permanently blocking streaming .
Fix: PauseStatePersistenceLayer now serializes ResponseStreamFilter via dumps() on every GraphRunPausedEvent and restores it via loads() on resume . Triggers: upstream if-else branch, a second Human Input in the same run, an Answer node that already streamed, or a variable written before the pause referenced post-resume.
2. Chat input disabled after Human Input completion (β€1.16.1) β Fixed in PR #38992 + PR #39485#
Symptom: After submitting a Human Input form in a published Chatflow WebApp, the chat input box remained permanently disabled (greyed out); a page refresh was required .
Root cause (backend): WorkflowAppQueueManager did not include QueueWorkflowPausedEvent in its terminal event set. When the SSE stream closed after pause, the listen() finally block called _abort_execution(), writing a Redis stop flag keyed on task_id. The resumed run inherited that flag and aborted, preventing delivery of the workflow_started event that would re-enable the input .
Fix: PR #39485 adds QueueWorkflowPausedEvent to the terminal set in WorkflowAppQueueManager, treating pause as ending only the current listener segment. PR #38992 fixes the frontend by adding handleResponding(true) in onWorkflowStarted so that isResponding is restored when a paused workflow resumes.
Why Chatflow vs. Workflow differed: The advanced-chat pipeline already published QueueAdvancedChatMessageEndEvent on pause (which IS in the terminal set), so Chatflow was less vulnerable to the backend abort β but both were affected by the frontend isResponding regression .
3. Resume aborts with "Stopped by user" β Fixed progressively in PR #39813 and PR #40905#
Symptom: Clicking Confirm on the Human Input form caused the resumed run to abort with "Stopped by user" .
Root cause: A resumed workflow reuses the paused run's task_id. Both cancellation channels β the generate_task_stopped:{task_id} Redis flag (TTL 600 s) and queued AbortCommand messages in the workflow:{task_id}:commands channel (TTL 3600 s) β are keyed by task_id. Signals written during the pause persist until resume .
Fix β PR #39813 : Introduced AppExecutionCoordinator (api/core/app/apps/execution_coordinator.py) to decouple the response-listener lifecycle from execution cancellation. Listener detachment is now an observation, not an abort trigger. The coordinator models PAUSED vs. TERMINAL states explicitly.
Fix β PR #40905 : Added clear_app_task_cancellation_signals(), called in _resume_app_execution() before any engine starts. It deletes the Redis stop flag and drains queued AbortCommand messages for the task_id, so no stale signals from before the pause can abort the resumed attempt. Both Workflow and Chatflow share this resume path.
4. Raw {{#...#}} placeholders after page refresh#
After a page refresh during a pending pause, the form may display unresolved variable placeholders. Root cause: render_form_content_before_submission() either failed to call convert_template() at pause time, or the API returned raw form_content instead of rendered_content .
5. Pointer event handling for chat input (β€1.16.0) β Fixed in PR #38385 and PR #38510#
The chat footer and inner layout were intercepting pointer events and blocking interactions. PR #38385 applied pointer-events-none to the container, restoring events only on interactive controls. PR #38510 followed up with a further refactor of the chat footer pointer-events pass-through.
Key Source Files Quick Reference#
| File | Description |
|---|---|
api/core/workflow/nodes/human_input/callback.py | DifyHITLCallback β core HITL bridge between Dify and graphon |
api/core/workflow/nodes/human_input/entities.py | Form schema and validation |
api/core/workflow/nodes/human_input/enums.py | Statuses, field types, button styles |
api/services/human_input_service.py | Form retrieval, submission, resume dispatch |
api/core/app/apps/execution_coordinator.py | AppExecutionCoordinator β per-attempt cancellation state |
api/core/app/apps/workflow/app_queue_manager.py | Workflow response listener; includes QueueWorkflowPausedEvent in terminal set |
api/core/app/layers/pause_state_persist_layer.py | PauseStatePersistenceLayer β serializes GraphRuntimeState and ResponseStreamFilter on pause |
web/app/components/base/chat/chat/answer/human-input-content/ | Chat-path rendering sub-components |
web/app/components/workflow/nodes/human-input/ | Node configuration panel and hooks |
web/app/components/base/chat/chat/hooks.ts | useChat β isResponding and human input form event management |