Picture the clerk at a service counter. A customer hands over an envelope. The clerk checks it is the kind of document the form asks for, opens it, copies what matters onto the form, and files the original where it belongs. If the envelope holds the wrong thing, the clerk hands it back and says what is needed instead. Nobody expects the clerk to accept a photo when the form says “PDF only”.
A file sent in chat is the same job for an AI employee. This post walks through each step as the Wetel docs describe it today: how the file arrives, how to check it, how to open it, and the limits. It is written for the operations lead who decides what the AI employee may accept, and for the developer who builds the workflow. A workspace owner decides what the workflow does with a file; the AI employee follows that workflow.
The short version
A file does not arrive as bytes. It arrives as a link, plus a declared type and file name, and the workflow decides whether and how to fetch it. Nothing is downloaded when the customer sends it. A later step fetches it, under the platform’s fetch rules, once earlier steps have checked that it is the right kind of file.
flowchart LR A["Customer sends a file<br/>in the conversation"] --> B{"Is there a file?"} B -- no --> Q["Carry on with<br/>the conversation"] B -- yes --> C{"Is it the type<br/>we accept?"} C -- no --> R["Ask the customer<br/>to resend"] C -- yes --> D["Fetch it: as text,<br/>or as a file to pass on"] D --> E["Use it or hand it<br/>to a business system"]How a file reaches the workflow
There are two documented ways, and both end in the same variables your workflow can read.
- A marker line from a channel connection. When a customer sends a photo or document through a messaging app connected to Wetel, the connection puts one line in front of the message text with the file’s link, declared type and file name. Wetel reads that line before the workflow starts, so no step has to pick it apart. Only a marker at the very start of the message counts: a marker typed after any real text is ignored, which stops a customer from typing a fake one to redirect a file.
- Structured attachments on the message. Your own app can send files as fields instead: up to 10 per message, each with a link and optionally a type, file name and kind (
IMAGE,DOCUMENT,AUDIO,VIDEOorOTHER). When attachments are present they take precedence over any marker in the text. A message may then have no text at all. The changelog says where this is live.
Either way, the workflow sees the first file in three flat variables, attachmentUrl, attachmentMime and attachmentFilename, which are empty strings when no file was sent. With structured attachments it also gets the full list in attachments and the image links in attachmentImageUrls.
A voice note on a channel connection is a different case: it is transcribed using the agent’s language settings, and the text reaches the workflow. See Voice notes for an AI employee.
Step 1: check there is a file at all
Start with a condition step that asks whether attachmentUrl is empty. The recipe uses exactly this expression:
{ "expression": "attachmentUrl != \"\"" }The true path goes to the file handling; the false path carries on with the normal conversation. Use the flat variables, not a nested object such as attachment.url, in a condition: the condition syntax has no property access, and the flat ones are always present. If a condition names a variable the run does not hold, it now takes the false path instead of failing, and the run record lists the missing name.
Step 2: check the type before you touch the file
Add a second condition step on attachmentMime and send anything you do not accept down a path that asks the customer to resend.
{ "expression": "attachmentMime == \"application/pdf\"" }The docs are firm on the order: check the type before fetching. A customer who sends a photo of a form, when your business system needs a PDF, should be asked for a PDF, rather than have the write fail, or worse succeed with unusable data, further down.
Two honest caveats about that type:
- It is declared, not verified. It comes from the sender, or is worked out from the file name or the link’s extension when the sender gave none. The fetch step does not check the bytes against it. If you must be sure a file really is a PDF, check the fetched content in a later step.
- For an image link with no extension, send
kind: IMAGEor a type with it, or it is classed asOTHERand left out of the image list.
There is no size field to check in advance. Size is enforced when the file is fetched (see the limits below), so plan the failure path at the fetch step instead.
Step 3: open it
An action step fetches the file in one of two modes, chosen by what you want to do with it.
- To read its contents, set
fetchAsBase64Varto the variable holding the link andextractTextVarto the name for the text. The step fetches the file and writes plain text, for PDF, DOCX, Markdown, CSV and XLSX files, which a laterllmstep can read. Text is capped at 50,000 characters by default (up to 200,000 if you set it), and a companion variable says whether it was cut short, so the workflow can say “this document is too long” instead of answering from half of it. - To pass it on unchanged, set only
fetchAsBase64Var. The step writes the file, encoded as base64, into its output variable, ready for a step that sends it to a business system expecting a file field.
{ "id": "fetch_file", "type": "action", "label": "Fetch File (base64)", "config": { "outputVar": "fileBase64", "fetchAsBase64Var": "attachmentUrl" }}The fetch is a plain download with no credential attached, through the same guarded outbound path as every other call: private addresses are refused. Point it only at links meant to be fetched that way, such as the link a messaging app gives for a file, which works for a limited time.
Photos are different. An llm step can fetch and read images itself; with structured attachments you name attachmentImageUrls and no extra step is needed. That has its own limits and checks, covered in How an AI employee reads a photo.
Step 4: file it, and check that it was filed
The step that sends the file on is an ordinary action step, and its run record entry shows the request it sent and any refusal. The customer only sees a generic apology when a call fails; the real reason, including the business system’s own response, is in the record. Before telling a customer “your document has been filed”, make sure the workflow only says so after the step that files it, and read a few run records to confirm it really did. What is a run record, and how do I read one? explains how.
When the file is missing, unsupported or too large
Each case has a documented outcome; design a reply for each one.
| What happened | What the platform does | What you add |
|---|---|---|
| No file sent | attachmentUrl is an empty string | The false path of the presence check |
| Type you do not accept | Nothing, unless you check | The type check and a “please resend” reply |
| File over 10 MB | The fetch fails the step; it does not cut the file short | A failure path on the fetch step |
| Text extraction of an unsupported type | The step fails, naming the supported types | Check the type first; a failure path |
| Document longer than the text cap | Text is cut at the cap and the “truncated” flag is set | A branch on the flag |
More than 10 attachments, or a link with user:password@ | The message is refused, without echoing the link back | Validate in your own app before sending |
An action step can have two labelled exits, success and failure. With them, any error the step throws goes down the failure path, where you can ask for a smaller file or a different format, instead of failing the whole run with a generic apology.
What history keeps, and what is hidden
The attachments are saved with the message, and read back with the sensitive part of the link removed. A message’s attachments show the link with its query string, fragment and any credentials stripped, ending in ?[redacted]. The same applies to the file links in a run’s context. The stored link is untouched, so a workflow step can still fetch a signed link. The changelog says where this is live.
Any extra context your app sends with a message is never returned on the message, but it does appear in the run’s context for people signed in to your workspace, so never put secrets in it. Images sent earlier in the same conversation stay visible to later turns, so a customer can send a picture in one message and ask about it in the next. A new conversation starts clean; memory across conversations is not built.
What the AI employee will not do
- It will not promise accuracy. We publish no accuracy figure for reading documents or photos; test on your own files before relying on them.
- It will not verify a file is what it claims to be unless your workflow checks the content.
- It will not fetch private addresses, and it cannot read a file whose link has expired: a messaging app’s file link works for a limited time.
- It will not decide what to accept. Your workflow decides, and for anything consequential, such as a claim or a change to a record, you can add an approval step so a workspace owner checks first. It is not on by default.
A pre-launch checklist
- Presence and type checks come first. A
conditiononattachmentUrl, then one onattachmentMime, before any fetch. - Every “no” has a polite reply. Wrong type, too large, unreadable and too long each lead to a message that tells the customer what to send instead.
- Fetch steps have failure paths.
successandfailureexits on everyactionstep that fetches or sends a file. - The workflow says “filed” only after the filing step, and you have read the run records of a few test sends to confirm it.
- Nothing secret travels in a message. No credentials in links you send, in the context your app passes, or in step requests.
Related reference docs
- Recipe: Channel Agent with File Intake: a full workflow that accepts a document, checks it and sends it on
- Sending files and context: structured attachments, their limits and what the workflow sees
- Channels: attachment markers: the marker line from a channel connection and the variables it fills
- Action step: fetching a file: the download mode, its 10 MB cap and what it does not check
- Action step: extracting text: the supported formats, the text cap and the truncation flag
- Action step: success and failure exits: routing an error to your own reply
- Condition step: the expression syntax and what a missing value does
To build one end to end, follow the file-intake recipe.