Asset Upload¶
The asset-upload tool lets a no-code agent ask a user to upload a document or image, store the bytes in object storage, and remember the asset so the agent and the user can find it again later.
It's the platform's first bi-directional tool: the agent prompts; the user responds in the portal; the agent gets the result and continues the conversation with full knowledge of the file.
When to use it¶
Reach for the asset-upload tool when:
- The agent needs to look at a file the user already has (a business plan PDF, a recipe photo, a receipt scan).
- The user wants the file to persist — tied to their per-agent memory, retrievable later.
- You want the file to be citable in subsequent turns — by filename, by ID, or via the asset's URN.
It's not the right tool for one-off image attachments inside chat messages (use the message attachment surface for that, when it exists), or for documents that need to be public (assets are private to the user's per-agent memory).
Turning it on¶
Asset upload is opt-in per agent, default off — the same shape as the email tool. An org admin enables it once per agent:
- Open the agent detail page.
- Switch to the Settings tab.
- In the Capabilities section, check Allow this agent to ask users for uploads. The change saves on click.
Behind the scenes, this adds the canonical key asset:upload to
the agentTools allowlist on the App that shares the agent's URN
(the agent's caller-identity App). Until that toggle is on, any
attempt to invoke the upload tool returns TOOL_NOT_ENABLED and
the chat continues without the prompt being shown to the user.
The mutation behind the toggle (setAgentAssetUploadEnabled) is
gated on org admin role. Builders without admin will see the
checkbox but receive a PERMISSION_DENIED error on save.
For a step-by-step walk-through with a verification checklist, see the how-to guide.
What an upload looks like¶
Once the tool is enabled, the agent invokes a request during the chat turn. It supplies a human-readable prompt and (optionally) the MIME types and maximum size it expects.
The user picks a file, confirms, and the bytes go directly from the browser to object storage. The server validates what landed, persists the asset metadata, and returns the new asset's id to the agent. From the agent's perspective the result is one of:
uploaded— the bytes are in. The agent getsassetId,filename,mimeType, andsizeBytes.declined— the user dismissed the picker.timeout— the user didn't respond in time (5 min default).
The user is never asked to retry on the agent's behalf; if they declined, the agent has to decide whether to ask differently or move on.
What the platform stores¶
Each upload becomes an Asset row tied to the user's per-agent
memory:
| Field | What it is |
|---|---|
filename |
Original filename (preserved verbatim). |
mimeType |
Verified against the file's actual Content-Type after upload. |
sizeBytes |
Verified against HeadObject.contentLength. |
storageKey |
Pointer to the bytes in object storage. Encrypted at rest in encrypted memories. |
scanStatus |
PENDING / CLEAN / BLOCKED. Downloads gated on CLEAN. |
description |
Optional, agent- or user-provided. |
uploadedAt |
When the upload completed. |
uploadedBy |
The user who uploaded. |
The asset has a stable URN: <memory-urn>:assets:<asset-id>. The
agent (or the portal) can use this URN to refer back to the asset
later — for download, for citation, for anything else.
What the platform DOES NOT store in the row¶
- The original filename in the storage key. The key uses
{org}/{memory}/{assetId}.{ext}— predictable, cleanable, free of user-supplied path-shaped characters. - The user-supplied filename anywhere it could leak across organizations.
- The bytes themselves anywhere except object storage. The database row is metadata only.
Listing prior uploads¶
The agent can list assets in the user's per-agent memory at any time, paginated, optionally filtered by MIME type. Results are ordered newest first; soft-deleted assets are excluded by default.
A common pattern: when the user mentions something the agent
remembers ("you sent me your business plan a few weeks ago"), the
agent calls agentAssets(mimeType: "application/pdf") to find
candidates and references them in its next message.
Downloading¶
When the agent (or the portal) needs the bytes — to embed in a prompt, to show to the user, to hand off to a content-extraction follow-up — it asks the platform for a short-TTL presigned URL and forwards that URL to the requester.
Default TTL: 5 minutes. Maximum: 1 hour. The URL is single-use within its lifetime and is never persisted. If the agent needs the URL again later, it requests a fresh one.
Downloads are gated on scanStatus = CLEAN:
PENDINGreturnsSCAN_PENDING— try again in a few seconds.BLOCKEDreturnsSCAN_BLOCKED— the asset failed virus scanning and cannot be downloaded.CLEANreturns the URL.
Limits¶
The defaults below apply when the agent does not specify limits. Builders can tighten per-agent or per-call:
- Maximum size: 50 MB.
- Allowed MIME types: PDF, common image formats (JPEG, PNG, GIF, WebP), plain text, common Office formats.
- Maximum URL TTL: 1 hour.
- Upload prompt timeout: 5 minutes.
The presigned upload URL itself pins the size and MIME type — if the user tries to upload a larger or different file, object storage rejects the PUT and the asset row is never written.
Soft-delete and restore¶
Assets soft-delete: the user (or an org admin) marks an asset
deleted, the row's deletedAt is set, and the asset disappears
from listings. The bytes are retained in object storage for 24
hours, during which the asset can be restored.
After 24h, a janitor sweeps the soft-deleted assets:
- The R2 object is deleted.
- The asset row is hard-deleted.
Trying to restore an asset whose 24h window has passed (or whose
bytes the janitor has already swept) returns ASSET_BYTES_GONE.
The platform does not silently re-create empty asset rows — the
user knows the file is irrecoverable.
Encrypted memories¶
For Tier 2.5 encrypted memories (Know Yourself, eventually
Tickfinder), the asset's storageKey is encrypted at rest with
the same AES-256-GCM machinery that protects other sensitive
fields. The decrypted form lives only on the request stack of a
download URL mint.
If a user tries to download an asset from an encrypted memory without their session key cached (e.g. their session expired), the platform surfaces the same plain-language message the edge-conditions feature uses: "This conversation needs your password to unlock your private memory before it can continue. Please re-enter your password and try again."
What's deferred¶
These are real follow-ups, not handwaving — each gets its own spec when the v1 ships and demand surfaces:
- Content extraction. OCR for PDFs and images, with extracted text saved on the asset for retrieval. The asset model already has a place for it; the pipeline is a separate change.
- Cross-memory sharing. An asset is bound to one memory in v1. Sharing across users or agents needs explicit policy.
- Asset → Reference promotion. Some uploads (a scanned book page, an uploaded paper) deserve to live in the citation graph. A future flow will ask "Add this upload as a reference?" at upload time.
- Customer-managed encryption keys for the bytes themselves. v1 uses object-storage server-side encryption (provider-managed keys); the HIPAA/CMK tier is its own spec.
- Versioning. v1 lets a user replace; the prior version is not retained.
Related¶
- Conversation routing — how agents invoke tools mid-conversation.
- The asset-upload spec lives at
spec-kits/specs/004-asset-upload/. The brainstorming session that settled the design is athadron-concept/brainstorming/2026-04-28-asset-upload-design/010-summary.md.