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 Attachments work the same way in the Python and TypeScript SDKs, and both write the same attachment format.

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, also enable URL uploads 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 in Python (url_upload_enabled requires openlayer>=0.17.9) and openlayer>=0.32.0 in TypeScript. Uploading media sends that data to Openlayer, so enable uploads only when your privacy requirements allow it.

Attach media to a step

Call log_attachment() in Python or logAttachment() in TypeScript inside any traced function to attach media to the step currently being recorded:
The Python helpers live in openlayer.lib.tracing, not in openlayer.lib. In TypeScript, logAttachment is exported from openlayer/lib/tracing/tracer.
The helper accepts a file path, raw bytes, or an Attachment you built yourself. Python also accepts a file-like object, and TypeScript accepts a Buffer, Uint8Array, or ArrayBuffer:
Every attachment can carry metadata. 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 the helper:
from_file() / fromFile() records the file’s absolute local path in the trace (filePath). If you don’t want local paths in your traces, read the file and use from_bytes() / fromBytes() instead.

Media in inputs and outputs

Attachments don’t have to hang off a step. You can pass one to a traced function, or return one, and it is uploaded and displayed like any other attachment:
The platform renders an input or output as media when the value is an attachment, when it is an object whose values are attachments (for example { "audio": attachment }), or when it is an array of content items. A content item nested inside another object is not rendered as media — put the bare attachment there instead.

Multimodal messages

To model a message that is itself part text and part media — the shape a vision or audio model actually receives — use content items:
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

Some integrations attach media for you once uploads are enabled.

OpenAI (Python)

If you send multimodal messages through a traced OpenAI client in Python, Openlayer reads the content array and converts it to attachments, on both the Chat Completions and Responses APIs:
Python
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.

Azure AI Speech (Python and TypeScript)

The Azure AI Speech integration attaches synthesized audio to each synthesis step, and the audio you pass for recognition to each recognition step, in both SDKs.

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.

How uploads work

  • When: attachments are uploaded when the trace completes, before the trace is published, so the published trace already carries each attachment’s storageUri.
  • Deduplication: attachments are deduplicated by MD5 checksum, so attaching the same image to three steps costs one upload.
  • Failures: a failed upload is logged and the trace is still published — you get the trace without that media rather than an exception.
  • Where: everything found on a step’s attachments and in its inputs and outputs is uploaded, including nested steps.

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 both SDKs write:
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 and the storageUri to record. How you upload depends on your deployment’s storage:
  • Openlayer Cloud (S3): the response also contains form fields. Send a multipart POST to url with every field first and the file last, in a part named file.
  • Self-hosted on GCS, Azure Blob Storage, or Oracle Object Storage: there are no fields. Send a PUT of the raw bytes to url, with a Content-Type header. Azure also requires x-ms-blob-type: BlockBlob.
  • Self-hosted with local storage: send a multipart POST to url with the file in a part named file.
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 answered by a model:
Run it with your credentials set, and the trace arrives with the attachments you logged, the media passed in as inputs, and the media in the output. In Python, the receipt image OpenAI received is attached too.

Troubleshooting

Check that uploads are enabled first (attachment_upload_enabled=True in Python, attachmentUploadEnabled: true in TypeScript) — they are off by default, and without them nothing is uploaded. If the attachment came from from_url() / fromUrl(), you also need URL uploads enabled, otherwise Openlayer never fetches a copy it can display.
Check where it sits. An input or output renders as media when it is an attachment, an object whose values are attachments, or an array of content items. A single content item nested inside another object is not rendered — use the bare attachment there instead.
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. In Python, enable debug logging while you debug:
Uploads happen when the trace completes, after your traced function returns, so your code isn’t held up while the media uploads. 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.