Skip to main content

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 needs ui.features.attachments: true and ui.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 image attachment.

Caps enforced by the composer:

CapValue
Per-file size10 MB
Cumulative per message25 MB
File count10 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:

  1. 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.

  2. The file is also written into the workdir, if the app has one. When runtime.workdir_mode is anything other than none, every attachment on the latest user message is copied to attachments/<name> inside the session's workdir. session.attachments (path, mime, size for each file) is available to a context: section's template: field - see below for how to surface it in the agent's context every turn. With filesystem granted, the agent can also come back to a specific file later - Read a slice by line range, Grep across 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.yaml
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.yaml
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:

  1. 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).

  2. Is the category allowed? app.attachments must include the file's category (image, document, audio, video). A category outside that list never reaches the composer's picker in the first place.

  3. Is the format one docextract supports? 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.

  4. 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 filesystem granted and a workdir (workdir_mode other than none):

    yaml
    capabilities:
    grant:
    - module: filesystem
    tools: [read, glob, grep]

    Without the grant, Read returns a permission error and the agent gives up.

Going further​