Input attachments
Letting customers send files
An input attachment is a file or image a customer sends with a message: a photo of a broken product, or a receipt that helps a tool find the order. Parlant never stores the file itself. Your frontend uploads it to the storage you already use, and Parlant carries a small reference to it inside the conversation. Your agent's tools read that reference and act on it.
- Why Parlant carries a reference to a file, never the file itself
- How your frontend attaches a file to a customer message
- What the model sees, and why captions matter
- How a tool reads attachments, and which checks stay yours
Parlant supports this with one idea: it carries references, never your files. The file stays in your storage.
This page covers two sides of that: the frontend code that sends uploads, and the tool that reads what was uploaded. Your coding agent handles both.
To follow along, it helps to know that a session in Parlant is one conversation between a customer and an agent, stored as a log of events. A customer message is one such event.
Here's the flow in practice. Your frontend uploads the file wherever it already keeps files, and gets a URL back. Parlant then passes a small, typed reference to that URL through the session, as part of the customer's message event.
That's deliberate. File storage means upload endpoints, size limits, access control, and cleanup. You've almost certainly done that work already, under rules Parlant can't know. So when a tool needs the file, it uses the URL, often passing it straight on to a vision model (a model that can read images) without downloading anything.
The attachment reference
Since Parlant carries references rather than files, here is what a reference actually contains.
The attachment reference lets your agent's tools find a file after your upload service has stored it. It carries the file's location, along with a short description, on the customer's message. Your frontend builds it as a small JSON object.
It has one required field, url, plus four optional ones: media_type, caption, filename, and metadata. The metadata field is free-form data for your own tools and frontend; Parlant carries it unchanged. In the SDK, the same shape is the AttachmentRef type:
class AttachmentRef(TypedDict):
url: str # where the file lives, in your storage
media_type: NotRequired[str] # IANA type: image/jpeg, application/pdf, ...
caption: NotRequired[str] # a short description, for people and the model
filename: NotRequired[str] # original name, for display and downloads
metadata: NotRequired[Mapping[str, JSONSerializable]]
Note that the URL can be public or signed (a temporary link that expires). Either way, Parlant never opens it.
Attaching a file to a message
Once your frontend has built a reference, it needs to get it into the conversation.
Posting a customer message means sending an event with kind: "message" and source: "customer" to the session's events endpoint. Attachments ride on that same request, in an attachments list. There is no separate upload call to Parlant.
In other words, your frontend uploads the file to your storage first, then adds the reference when it posts the customer's message:
POST /sessions/{session_id}/events
Content-Type: application/json
{
"kind": "message",
"source": "customer",
"message": "Here's the leak under my sink.",
"attachments": [
{
"url": "https://cdn.example.com/u/9f3a.jpg",
"media_type": "image/jpeg",
"caption": "photo of a leaking sink pipe"
}
]
}
A message may carry attachments with no text at all; a customer just sending a photo is a valid message.
Note that posting attachments needs the create_customer_event_with_attachments permission. Permissions are what Parlant's authorization policy grants each kind of caller, and the default policy grants this one to customers and guests.
The official chat widget supports attaching files from its composer. Connect its upload handling to your storage so it can send the resulting references. Frontend integration shows how to run it.
What the model sees
With the reference posted, the next question is what your agent actually gets to see.
The model never receives the file. What it does see decides whether the agent can react to the attachment at all, so the fields you fill in matter.
For each attachment, Parlant appends a short note to the message text. It uses the caption when present, then the filename, and marks the kind from media_type:
[image: photo of a leaking sink pipe]when there's a caption and animage/*type[file: receipt.pdf]when there's only a filename[image attached]or[file attached]when there's neither
The model never sees url or metadata; those are for your tools and frontend. Write captions with that in mind.
Why captions matter for policies
A policy is a plain-language rule your agent follows in a given situation. Consider a policy like "when the customer sends a photo of a problem, open a repair ticket."
A good caption, say photo of a leaking sink pipe, is what lets that policy match. Without one, the model sees only [image attached], and has far less to go on.
Designing policies covers them.
Note that moderation checks message text only. Captions and filenames reach the model unmoderated, so treat them as untrusted if customers can write them freely.
Reading attachments in a tool
Once the model knows a file is there, a tool gets to it like this.
A tool is a Python function your agent can call during a turn, declared with @p.tool. While it runs, p.Session.current is the session being handled and p.Customer.current is the customer in it, so the tool needs no arguments to find the conversation.
Your tool reads the attachments off the latest customer message, through interaction.last_customer_message. It returns a p.ToolResult, whose data is the text the model reads next.
In the example below, the tool forwards the URLs to your CRM without downloading anything. It also handles the no-photo case, by telling the model what to do next. Ask your coding agent for a tool like this:
import parlant.sdk as p
@p.tool
async def open_repair_ticket() -> p.ToolResult:
last = p.Session.current.interaction.last_customer_message # None before the first customer message
attachments = last.attachments if last else [] # the AttachmentRef list, as posted
images = [a for a in attachments if a.get("media_type", "").startswith("image/")]
if not images:
return p.ToolResult(
data="No photo found on the customer's message. Ask them to attach a photo of the problem."
)
ticket = await crm.create_ticket(
customer_id=p.Customer.current.id,
photos=[a["url"] for a in images],
)
return p.ToolResult(data=f"Opened repair ticket {ticket.id}.") # what the model reads and can tell the customer
To put this tool to work, attach open_repair_ticket to a policy that tells the agent when to use it, such as when a customer sends a photo of a repair problem. Here, crm is your ticketing client.
Note that both outcomes give the model something to act on. A successful result supplies the ticket reference; the no-photo result asks the customer for the missing input.
Using tools covers tool results in full.
Limits worth knowing
Parlant only carries the reference. That means the checks a file upload normally gets are yours to do. These are the ones that matter.
- URLs are yours to protect. Parlant treats
urlas text. If a file must stay private, give it a signed URL that expires. - Parlant doesn't validate attachments: no count limit, no size check, no scheme check. Validate at your edge, and only accept URLs from your own storage.
- You provide
media_type; Parlant never sees the file, so it can't confirm the type. Check it yourself if it matters. - When a tool downloads a URL, that's your network call. Allow only known hosts and set timeouts. A URL from an attacker is a server-side request forgery risk.
- Attachments belong to single messages in one session. They aren't long-term customer memory; Cross-session memory covers that.