Skip to main content
Not every AI system runs on text alone. A claims agent reads a photo of a receipt, a support bot listens to a voice note, a document pipeline parses a PDF. Attachments let your traces carry that unstructured data. The media itself is uploaded to your workspace storage, and the trace keeps a reference to it — so the platform can show you the actual image, play the actual audio, and page through the actual document next to the rest of the trace. A monitoring record whose trace shows an audio recording and a document alongside the generated text

Enable attachment uploads

Attachment uploads are disabled by default. Until you turn them on, attachments are recorded in the trace but never uploaded — and anything that was not uploaded cannot be displayed.
With uploads enabled, Openlayer uploads each attachment when the trace completes and stores the resulting reference in the trace data. If you attach media that already lives at an external URL, add url_upload_enabled so Openlayer fetches it and keeps its own copy:
Without it, an external URL is recorded as-is. That keeps your trace pointing at a resource Openlayer cannot read — if the URL later expires or sits behind authentication, the media is gone.
Attachments require openlayer>=0.17.0, and url_upload_enabled requires openlayer>=0.17.9.

Attach a file to a step

Call log_attachment() inside any traced function to attach media to the step currently being recorded:
The multimodal helpers live in openlayer.lib.tracing, not in openlayer.lib.
log_attachment() accepts a file path, raw bytes, a file-like object, or an Attachment you built yourself:
Every attachment can carry a metadata dict. Use it for whatever you need to filter or debug on later: the upload channel, a page count, an audio duration, a document revision.

Build attachments explicitly

For more control, construct an Attachment and pass it to log_attachment():
Identical files are uploaded once per trace — attachments are deduplicated by MD5 checksum, so attaching the same image to three steps costs one upload.

Multimodal inputs and outputs

The attachments above hang off a step as supporting artifacts. To model a message that is itself part text and part media — the shape a vision or audio model actually receives — use content items instead:
There are four content items — TextContent, ImageContent, AudioContent, and FileContent — and a single message can mix as many as you need. Openlayer renders the message in order, so the text and the media it refers to stay together.

Automatic capture from OpenAI

If you send multimodal messages through a traced OpenAI client, you do not need to write any of the above. Openlayer reads the content array and converts it to attachments for you, on both the Chat Completions and Responses APIs:
Images (image_url, input_image), audio (input_audio), and files (file) are all recognized, whether they arrive as a URL, a base64 data URL, or an uploaded file ID. Generated images in Responses API output are captured the same way.

How attachments appear in Openlayer

Uploaded attachments are rendered wherever the trace is shown — in the row, in the row detail view, and on the individual step: Every attachment can be downloaded, whatever its type. Downloads keep the attachment’s name and extension as part of the filename.

The attachment format

Attachments are plain JSON inside your trace, so any client that can publish a row can publish an attachment. This is what the SDK writes:
storageUri is what makes an attachment displayable — it is the reference to the copy in your workspace storage. To obtain one for media you upload yourself, request a presigned URL, upload the bytes to it, and keep the storageUri that comes back:
The response contains the url to upload to, any form fields that upload requires, and the storageUri to record. You can then put the attachment in a column when you stream the row:
The column holding the attachment must be listed in inputVariableNames. A row containing an attachment in an undeclared column is still accepted — the request returns success — but the platform has no column to attach it to, so it is never displayed.
A column can also hold a full multimodal message, mixing media with text:

Complete example

An expense claim that arrives as a photo, a voice note, and a policy document — attached to the trace, then read by a vision model:
Run it with your credentials set, and the trace arrives with four attachments: the three you attached explicitly, plus the receipt image OpenAI received.

Troubleshooting

Check attachment_upload_enabled=True first — it is off by default, and without it nothing is uploaded. If the attachment came from Attachment.from_url(), you also need url_upload_enabled=True, otherwise Openlayer never fetches a copy it can display.
Attachments with no data and no reference are dropped rather than published — most often because a file path does not exist. The SDK logs a warning when this happens, so enable logging while you debug:
No. Uploads happen when the trace completes, on a background thread, so your request is not held up. A failed upload is logged and the trace is still published — you get the trace without the media rather than an exception.
Looking to add non-media context to your traces instead? See Add metadata to traces.