Attach files to a chat session
The chat composer ships with a paperclip menu that lets the
user drag-drop or pick files for the current message. Configure
what the composer accepts in app YAML (app.attachments,
ui.features.attachments, ui.composer.file_upload).
End-user flow
The composer accepts files in three ways:
- Paperclip button: opens the native file picker, scoped
to the categories declared in
app.attachments(image,document,audio,video, or*/all). Also needsui.features.attachments: trueandui.composer.file_upload: true- see Client manifest. - Drag-and-drop: drop a file or a batch anywhere over the message area while the composer is focused.
- Paste from clipboard: a screenshot pasted from the
system clipboard counts as an
imageattachment.
Caps enforced by the composer:
| Cap | Value |
|---|---|
| Per-file size | 10 MB |
| Cumulative per message | 25 MB |
| File count | 10 files |
Files that fail any check are dropped with an inline toast in the composer. Nothing reaches the daemon until the user hits send.
What happens when a user attaches a file
Each file becomes a hash-addressed blob attached to the user message. From there, two things happen - one always, one only when the app has a real workdir:
-
The content reaches the agent's next turn automatically. No tool call needed. Images, audio, and video are attached as native multimodal content parts. Plain text and structured text formats (
text/*, JSON, YAML, XML, CSV, JS, SQL, ...) are inlined directly into the message, up to 256 KB. PDF, DOCX, PPTX, and XLSX are converted to plain text first (their raw text content - no page or section metadata is preserved) and inlined the same way. If a PDF or image doesn't yield text this way (a scanned page, for instance) and an OCR backend is configured, it is queued for OCR and merged in once ready. -
The file is also written into the workdir, if the app has one. When
runtime.workdir_modeis anything other thannone, every attachment on the latest user message is copied toattachments/<name>inside the session's workdir.session.attachments(path, mime, size for each file) is available to acontext:section'stemplate:field - see below for how to surface it in the agent's context every turn. Withfilesystemgranted, the agent can also come back to a specific file later -Reada slice by line range,Grepacross several attachments - instead of relying only on what was inlined into the first turn.
With workdir_mode: none the first path still works (the agent
sees the content the turn it was attached), but nothing is ever
written to disk - there is no workdir to write into, so a
filesystem grant has nothing to read.
app:
app_id: simple-chat
name: Simple Chat
attachments: [document]
runtime:
mode: conversation
workdir_mode: none
agents:
- id: main
role: assistant
brain:
provider: ollama
model: qwen25-7b-gpu:latest
backend: openai_compat
config:
base_url: http://localhost:11434/v1
api_key: ollama
system_prompt: |
You answer questions about the documents the user
attaches. Cite filenames in brackets, e.g. [report.pdf].
Giving the agent a workdir to re-read attachments from
For a bigger corpus, or when the agent needs to reference a
specific line range after the first turn, give the app a real
workdir and grant filesystem so it can come back to the files
directly:
app:
app_id: doc-analyst
name: Doc Analyst
attachments: [document]
runtime:
mode: conversation
workdir_mode: auto
context:
sections:
- id: current_attachments
title: "Attached files"
template: "Files attached to the latest user message: {{session.attachments}}"
when: session.attachments
priority: 40
agents:
- id: main
role: assistant
brain:
provider: ollama
model: qwen25-7b-gpu:latest
backend: openai_compat
config:
base_url: http://localhost:11434/v1
api_key: ollama
system_prompt: |
You analyse attached documents. Use Read when you need a
precise quote by line range, and cite as [filename] or
[filename · lines A-B].
tools:
modules:
filesystem: {}
capabilities:
default_policy: auto
grant:
- module: filesystem
tools: [read, glob, grep]
{{session.attachments}} only resolves inside a context:
section's template: field - it does nothing inside
system_prompt:, which is used verbatim with no per-turn
substitution. The when: session.attachments guard means this
section is skipped entirely on turns with nothing attached,
instead of injecting an empty line.
filesystem is scoped to the session's workdir by default (see
Workdir sandbox) - the
grant above sees attachments/ alongside anything else in that
workdir, not the rest of the filesystem.
Troubleshooting
"The agent doesn't see my file"
Symptoms: the user uploaded a file, the chat went through, but the agent answers like nothing was attached.
Check, in order:
-
Did the upload succeed? In the Digitorn UI, confirm the file chip shows on the sent message rather than an error toast (oversized file, wrong category, or over the 10-file cap all get rejected client-side before send).
-
Is the category allowed?
app.attachmentsmust include the file's category (image,document,audio,video). A category outside that list never reaches the composer's picker in the first place. -
Is the format one
docextractsupports? Only PDF, DOCX, PPTX, and XLSX are converted to text automatically; anything else falls back to OCR (if configured) or arrives as a raw binary part the model may not be able to read. -
Is the agent allowed to re-read it later? If the answer should come from a specific line range on a second turn, the agent needs
filesystemgranted and a workdir (workdir_modeother thannone):capabilities:
grant:
- module: filesystem
tools: [read, glob, grep]Without the grant,
Readreturns a permission error and the agent gives up.
Going further
- The schema field:
app.attachments - Client manifest - the
ui.features.attachments/ui.composer.file_uploadtoggles that show the paperclip button - Workdir sandbox - how
filesystemgets scoped to the session workdir - filesystem module