Agent File Handling#
Agent file handling in Dify covers how the DifyAgentNode (agent v2) manages file data across three phases:
- Input propagation β forwarding user-uploaded or workflow-injected files to the agent backend runtime
- Output normalization β converting raw backend payloads into typed
FileSegment/ArrayFileSegmentvalues - Post-run validation & failure orchestration β type-checking declared file outputs and deciding whether to retry, apply defaults, or fail
All relevant server-side code lives under api/core/workflow/nodes/agent_v2/ :
| File | Role |
|---|---|
agent_node.py | Orchestrates the full run loop; wires together the subsystems below |
output_adapter.py | Normalizes raw backend output into workflow-facing file segments |
output_file_rebacker.py | Hydrates legacy {"id": "..."} ToolFile references from the database |
output_type_checker.py | Validates declared FILE/ARRAY[FILE] outputs against canonical format |
output_failure_orchestrator.py | Decides retry / default / fail strategy on type-check failures |
api/core/workflow/file_reference.py | Canonical dify-file-ref:β¦ reference format helpers |
The agent backend runtime (dify-agent) handles file parameters for plugin tools in dify-agent/src/dify_agent/layers/dify_plugin/tools_layer.py .
Runtime File Propagation (Inputs)#
Two separate input file paths feed the agent backend, both of which had gaps patched in recent releases :
-
Chat-uploaded files (Agent App mode): PR #37926 added
user_filestoAgentBackendAgentAppRunInputand routes them through aDifyUserPromptLayerConfig, so files reach the backend asBinaryContent. Before this fix, thefilesparameter was empty at execution time even when a file was visually accepted in the UI. -
Workflow
sys.files(Workflow/Chatflow mode): PR #38453 added_summarize_uploaded_workflow_files()inruntime_request_builder.py, which readsSystemVariableKey.FILESfrom the variable pool and injects them into the workflow context prompt. The early-return condition was also relaxed so the context is populated whenever uploaded files are present, even without an explicit user query text.
File access control: DatabaseFileAccessController checks that the created_by field on an uploaded file matches the user_id in the execution context. A mismatch causes a silent None return β the file is silently dropped from the files list . Ensure the same user identifier is used for both the file upload and the subsequent agent invocation.
Exception β in-run tool files: ToolFile records produced during the current workflow execution (e.g., by a tool or plugin node) are automatically granted access via grant_tool_file_access(), allowing them to be passed to later nodes even when their user_id differs from the end-user scope [PR #41300]. This specifically fixes the scenario where a workflow downloads or produces a file in one tool node and passes it to a later LLM vision node. When such a workflow is published and invoked as a tool from an agent under an end-user scope, the tool-produced files remain accessible across nodes within the same execution context. Files that are not granted during execution (e.g., files from external uploads or different executions) still require matching user_id.
Output Normalization#
WorkflowAgentOutputAdapter converts the terminal AgentBackendRunSucceededInternalEvent payload into a NodeRunResult. File normalization happens in _normalize_outputs(), which processes each key in the backend output dict and applies type-aware conversion only for fields declared as FILE or ARRAY[FILE] .
Three code paths handle file values:
- Already a
File/FileSegment/ArrayFileSegmentβ passed through unchanged. - Legacy
{"id": "<tool_file_id>"}β_normalize_legacy_tool_file_value()calls the injectedToolFileRebackerto hydrate the file from theToolFileDB row. Older agent backend builds may return this format. - Canonical declared mapping (
tool_file,local_file,datasource_file) β_normalize_declared_output_value()callsbuild_from_mapping()with the resolvedtenant_id. Aremote_urlmapping only requires{"transfer_method": "remote_url", "url": "..."}with notenant_idneeded. For persisted transfer methods,tenant_idis mandatory β the adapter raisesValueErrorif it is missing.
tenant_id is resolved from either the call-site parameter or metadata["tenant_id"] set during backend run setup. The ToolFileRebacker is defined as a Protocol, making it injectable for testing.
ToolFile Rebacker#
reback_tool_file_output() in output_file_rebacker.py provides backwards-compatibility for agent backends that return output files as bare {"id": "<uuid>"} mappings instead of full canonical references .
The function:
- Validates the UUID format and enforces tenant ownership (
ToolFile.tenant_id == tenant_id); returnsNoneon any mismatch rather than fabricating a file with empty metadata. - Derives
mime_type,extension, andsizefrom theToolFileDB row β the sandbox payload is never trusted for metadata. - Produces a
Filewithtransfer_method=TOOL_FILEand a canonicaldify-file-ref:β¦reference built viabuild_file_reference().
This component was introduced as part of the Agent Files infrastructure in PR #37172 .
Output Type Checking & Failure Orchestration#
After a successful backend run, DifyAgentNode runs a per-output type check before constructing the final NodeRunResult .
Type Checker (output_type_checker.py) validates:
- The raw output is a JSON dict; if not, all declared outputs fail.
- Each declared
FILEfield has a valid canonicaldify-file-ref:β¦reference and a matching transfer method with tenant access verified. - Each element in
ARRAY[FILE]fields is validated individually.
The JSON schema transmitted to the agent backend was progressively tightened:
- PR #38183 introduced
AgentStubFileMappingto replace manualoneOfconstruction . - PR #38221 added
additionalProperties: falseand a regex constraint on canonical references to prevent agents from inventing references .
Failure Orchestrator (output_failure_orchestrator.py) determines what happens when type-check fails:
| Decision | Meaning |
|---|---|
RETRY | Re-invoke the agent backend (up to each output's retry budget) |
USE_DEFAULT | Replace failed outputs with declared default values |
TAKE_FAIL_BRANCH | Route through the node's fail branch edge |
FAIL_NODE | Mark the workflow node as failed |
The retry budget is the maximum across all failing outputs. When exhausted, the orchestrator merges per-output terminal strategies using precedence: FAIL_BRANCH > STOP > DEFAULT_VALUE. Defaults are patched into the event output via _patch_event_with_defaults() before the output adapter runs .
Plugin Tool File Transport (dify-agent)#
The agent backend runtime (dify-agent) handles FILE and FILES-typed plugin tool parameters in tools_layer.py .
_PluginToolFileContext resolves file inputs before tool invocation via three paths :
- Remote URL string β passed through as-is (requires
http://orhttps://prefix). - Sandbox path string β uploaded to Dify via a shell script calling
dify-agent file upload; returns atool_filemapping with a canonical reference. - File mapping dict β for
remote_urltransfer method, URL is used directly; forlocal_file,tool_file, ordatasource_file, callsPOST /inner/api/download/file/requestto get a signed download URL.
_cast_tool_parameter_value() handles the type dispatch :
FILEβ resolves a single file viato_plugin_file_parameter()(raises if a list with β 1 element is supplied).FILES/SYSTEM_FILESβ mapsto_plugin_file_parameter()over each list item.
All resolved files are normalized into {"dify_model_identity": "__dify__file__", "type": ..., "url": ...} dicts before being sent to the plugin daemon .
Tool response handling: _convert_tool_response_to_text() collapses daemon stream messages into a plain-text observation . IMAGE and IMAGE_LINK messages currently produce a static string ("image has been created and sent to user alreadyβ¦") β the image URL is not forwarded. See Known Issues.
Canonical File Reference Format#
All persisted file references in agent v2 contracts use the opaque format dify-file-ref:<base64url-json>, implemented in api/core/workflow/file_reference.py .
| Helper | Behavior |
|---|---|
build_file_reference(record_id) | Creates a canonical reference from a DB record ID |
is_canonical_file_reference(value) | Strict validator; rejects raw record IDs. Used by output_type_checker.py. |
parse_file_reference(value) | Lenient parser; accepts both canonical format and legacy raw record IDs for backward compatibility |
resolve_file_record_id(value) | Extracts the underlying record ID from either format |
The type checker enforces canonical format on new agent outputs. Legacy raw record IDs are only tolerated through the rebacker path for older backend builds .
Known Issues#
Tool-Generated Images Dropped in Agent App (open)#
PR #40455 (open) addresses two loss layers in the Agent App path :
_convert_tool_response_to_textdiscards the URL carried inIMAGE/IMAGE_LINKdaemon messages.AgentAppRunner/_AgentProcessRecorderhas no equivalent of the legacyToolFileMessageTransformer β ToolEngine._create_message_files β QueueMessageFileEventchain.
The proposed fix: the SDK reports URL-bearing image references on ToolReturnPart.metadata under the dify_tool_files key; the API side replays the legacy chain to create MessageFile rows and publish QueueMessageFileEvent, which feeds the existing SSE stream without frontend changes. Failures degrade gracefully to the text-only observation. The workflow agent v2 node path also loses images, but exposing them there requires file-typed node outputs (separate feature). As of the current date this PR is not yet merged .
Unsigned Tool File URLs β 422/403 Errors#
base_app_runner.py stores /files/tools/{id} as a bare unsigned path in MessageFile.url . Separately, ToolFileMessageTransformer.get_tool_file_url() returns /files/tools/{tool_file_id}{extension} unsigned. The /files/tools/ endpoint validates timestamp, nonce, and sign query parameters and rejects requests without them (422 Unprocessable Entity if missing/invalid; 403 Forbidden on signature verification failures).
The intended two-stage pattern: URLs are stored unsigned early, then message_file_utils.py calls sign_tool_file() at serialization time. Code paths that bypass message_file_utils.py expose unsigned URLs. The signing entry points are in api/core/tools/signature.py .
Agent Squid Proxy Blocking External File Domains#
When INTERNAL_FILES_URL is set to an empty string in Docker, the previous AliasChoices lookup selected the empty value instead of falling back to SERVER_CONSOLE_API_URL. Agent file URLs then resolved to the external hostname, which the agent Squid proxy blocked if it mapped to a private IP. Fixed (PR #39839): resolution order is now INTERNAL_FILES_URL β SERVER_CONSOLE_API_URL β FILES_URL, with the resolved file origin passed to agent_ssrf_proxy as a Squid exception .
File Disappears Due to User Identity Mismatch#
If the user identifier used for a file upload differs from the one used in the subsequent agent invocation, DatabaseFileAccessController silently returns None and the file is dropped . This is not agent-v2-specific β it affects any Dify app type. Ensure the same user value is used consistently across upload and invocation API calls.
Fixed for in-run tool files: ToolFile records produced during a workflow execution (e.g., by tool or plugin nodes) are automatically granted access via grant_tool_file_access() [PR #41300]. This allows workflow-as-tool scenarios where a file is downloaded/produced in one node and passed to a later LLM vision node, even when the published workflow is invoked from an agent under a different end-user scope. Files that are not granted during execution (external uploads, files from different executions) still require matching user_id.