Skip to main content
Media on a run is stored in the database as base64 by default, both what you send in and what the run produces. Set media_storage and the bytes go to object storage instead, leaving a MediaReference in the row.
The model still receives the media, tools still process it, and history still replays it on later runs. Only the storage location changes. media_storage works the same way on Agent, Team, and Workflow.

Backends

Each backend has an async twin: AsyncLocalMediaStorage, AsyncS3MediaStorage, AsyncGCSMediaStorage. A sync backend also works inside arun(), where the upload runs in a worker thread to keep the event loop free.

What Gets Offloaded

Every media object on a run, whichever direction it came from:

How It Works

  1. Media is uploaded to the backend before the run is written to the database, including any background status row or mid-run checkpoint written before the final one.
  2. The row stores a MediaReference (key, bucket, mime type, size, SHA-256) instead of the bytes.
  3. On a later run, the media is read back from the backend so the model sees it as before.
Offload runs on a deep copy, so the RunOutput you are handed keeps its bytes. Only the persisted copy carries the pointer.
media_storage requires store_media=True, which is the default. With store_media=False no new media is persisted, and an Agent or Team still reads back and deletes media stored earlier. A Workflow resuming a paused run needs store_media=True to refresh its executor’s media.
A bucket you do not own makes every upload fail. Offload falls back to inline base64 and the run still succeeds, so the failure is easy to miss. Check that media reaches the bucket the first time you configure it.

Teams and Workflows

Set media_storage on the team or workflow, not on its members. The parent owns the write, so its backend uploads the whole run including member and step rows. A member pointed at a different bucket cannot resolve the parent’s references and its media is skipped on the next turn. A member with store_media=False has its media dropped before the parent uploads anything. A restriction travels down the tree; store_media=True on the parent does not override a member that turned it off.

URL-only Media

Media that arrives as a bare URL is skipped during offload. Agno stores the URL and never downloads the file. Set persist_remote_urls=True on the backend to fetch the URL from your process and store the bytes as well. Enable it only for URLs you trust, since the fetch runs with your network reach.

Deleting Media

Offloaded media outlives the session by default. The reference in the row is the only record of which object belongs to which session, so deleting rows first leaves orphaned objects. Pass delete_media=True to read the keys off the rows before deleting them, then sweep the objects.
The flag exists on Agent, Team, and Workflow, in both sync and async variants. It is opt-in: a plain delete_session() leaves every object in the backend. Forking a session re-uploads the media under the fork’s own keys, so either session can be deleted without affecting the other.
A MediaReference records the backend and bucket that minted it. Media stored elsewhere is not read back, not deleted, and not served, so changing bucket leaves earlier objects reachable only by the old configuration.

AgentOS

AgentOS serves stored media at /sessions/{session_id}/media/{storage_key}, scoped to the caller’s session ownership. The backend is discovered from your agents, teams, and workflows.
Set media_storage on AgentOS when your entities use different backends, otherwise the first one found serves every request. The route streams the bytes by default, which keeps the bucket private and leaves one CORS surface. Pass redirect=true to get a 307 to a freshly-signed URL instead, which is the cheaper path for embedding media in a page. Backends that sign nothing still stream: local storage always, and GCS with no service-account key. The same delete_media flag works over HTTP, on one session or a batch:

Developer Resources