Blog · · 11 min read
AI agent artifact management: publish generated files as durable product artifacts

AI agent artifact management breaks when a runtime path is mistaken for a product file. The agent creates a report in /workspace, returns a sandbox link, and marks the task complete. Minutes later the container expires, another user cannot open the link, or a chat channel truncates the payload. A download helper fixes only the first hop. Each generated file needs an untrusted candidate record that moves through capture, validation, ownership, release, delivery, versioning, and deletion. This guide defines that lifecycle for engineers adding file-producing agents to an existing product.
Why generated files disappear or reach the wrong user
Agent runtimes optimize for execution, not durable product records. OpenAI's Code Interpreter documentation describes containers as ephemeral and tells clients to download files while the container is active. Anthropic's code execution documentation likewise separates temporary container data from files deliberately persisted through its Files API. A path inside either workspace is therefore a locator for retrieval, not a stable product URL.
The failure appears at the product boundary. In one Microsoft Q&A report about Assistant-generated files, a developer could not expose a sandbox path directly to users and asked whether the application had to copy the file into its own storage. An IronClaw issue describes the same class of failure in a messaging channel: the agent returned a local Markdown path instead of attaching the file, so the user could not retrieve it.
Transport adds a second failure mode. The A2A protocol specification defines task artifacts, file parts, and streaming artifact-update events, including append and final-chunk semantics. Those primitives move bytes and metadata between agents. They do not decide when a partial stream is complete enough to publish, who owns the resulting object, or whether the user is still allowed to read it.
Two issue reports expose different completeness failures. A Google ADK report describes streams that stopped in a working state without a final artifact, plus append events for an artifact that had not been created. An Agents at Scale report says a consumer retained only the final artifact even though the response contained several. These are author reports about specific implementations, not universal protocol defects. They still identify concrete cases your boundary must handle.
The product should never make the agent's completion claim authoritative. A run can finish while its file is missing, incomplete, private, unsafe, or undelivered.
Define one lifecycle outside the agent runtime
Use a state machine that lives in your product service, not in the model prompt or provider container:
CandidateArtifact
-> Captured
-> Validated
-> ReleaseDecision
-> PublishedVersion
-> DeliveryReceipt
-> RetainedOrDeleted
Each transition answers a different question:
CandidateArtifact: What did the runtime claim it produced?Captured: Did the product copy every expected byte before the source expired?Validated: Is the manifest complete, internally consistent, allowed, and safe to process?ReleaseDecision: May this candidate become visible, or must policy or a person reject it?PublishedVersion: Which immutable bytes are the official product artifact?DeliveryReceipt: Did the intended channel receive a usable reference to that version?RetainedOrDeleted: Where do the bytes and derivatives exist now, and when must each copy disappear?
The lifecycle separates task state from artifact state. An agent task may be completed while one candidate is rejected and another is published. The user-facing result should say exactly that. Do not collapse both records into a generic success flag.
Capture a complete manifest before the workspace expires
Create the artifact record as soon as the runtime announces a file, but keep it private. The capture worker should fetch from the provider, stream into quarantine storage, calculate a digest, and compare the received object with a manifest. A minimal schema can look like this:
{
"artifact_id": "art_01",
"run_id": "run_42",
"agent_release_id": "release_17",
"tenant_id": "tenant_9",
"owner_user_id": "user_3",
"source": {
"provider": "runtime-a",
"container_id": "container_8",
"file_id": "file_6"
},
"declared": {
"name": "quarterly-report.pdf",
"media_type": "application/pdf",
"role": "customer_report",
"part_index": 1,
"part_count": 1
},
"captured": {
"byte_count": 184302,
"sha256": "<digest>",
"storage_key": "quarantine/tenant_9/art_01/v1"
},
"state": "captured"
}
Do not trust the filename, media type, byte count, or part count supplied by the model. Compare declared values with the fetched object and your own parser results. If the runtime streams several parts, keep the candidate incomplete until all indexes arrive and the final marker agrees with the manifest. Reject an append event that references no known artifact instead of silently creating a different object.
Make capture idempotent. Use a uniqueness key such as (run_id, provider_file_id, artifact_version) and store the calculated digest. Reprocessing the same event should return the existing capture record. Different bytes under the same source identity should trigger a conflict, not overwrite the quarantine object.
Set a capture deadline shorter than the provider's expiry window. If the source cannot be fetched by that deadline, mark the artifact capture_failed and expose a typed recovery action. Retrying the entire agent run can repeat unrelated tool effects, so prefer a file-only recovery endpoint when the provider still has the object.
Validate content, ownership, and policy before release
Validation should happen against the captured object, never against a provider URL that can change or expire. Run the checks your product requires, which may include:
- calculated size and digest against the completed manifest;
- file-signature detection rather than trusting an extension;
- archive expansion limits and parser time or memory limits;
- malware or active-content scanning where the format and risk justify it;
- policy checks for secrets, regulated fields, tenant data, or prohibited output;
- a render or parser pass that proves the file is usable;
- product rules for allowed roles, formats, and maximum size;
- current tenant, owner, audience, and retention assignments.
These are product decisions. The agent may suggest a name, role, or intended audience, but it cannot grant access. Resolve the authenticated principal, tenant, and destination from server-side context. Recheck them at release time because a user's role or account state can change while a long agent run is active.
Some products can auto-release low-risk files. Others require a person to review a legal document, customer communication, executable archive, or externally shared report. A Mastra feature report makes this distinction explicit by requesting a post-execution gate that can keep an artifact private before task completion. Model this as approve, reject, request_changes, or expire; do not represent review as an editable Boolean.
Bind an approval to the candidate's digest, policy version, audience, and proposed filename. If any of those values change, the old approval no longer applies. That prevents a reviewed PDF from being replaced with different bytes before publication.
Publish one immutable version exactly once
Keep two records:
- a mutable product-level artifact, such as
quarterly-report, which tracks the current version and lifecycle policy; - an immutable artifact version, which identifies one digest, storage object, producer run, agent release, approval, and publication event.
Publish by copying or promoting the approved quarantine object into tenant-scoped durable storage, then commit the version record using one idempotency key. A crash after copying bytes but before committing metadata must be recoverable. A reconciliation worker should compare pending publish records with storage and either finish the transaction or remove an orphan, according to policy.
Never expose the durable storage key directly. Authorize every download against the current tenant, user, artifact state, and audience, then issue a short-lived signed URL or stream the object through the product. Record the version in the URL or request token so a later edit cannot change what an earlier approval referenced.
The publication event should contain enough evidence to answer:
- Which candidate digest was released?
- Which policy and approval allowed it?
- Which user and tenant own it?
- Which immutable version became current?
- Which destination was notified?
- Which run and agent release produced it?
This record is more useful than an agent transcript because it describes the actual bytes and the enforcement decisions around them.
Deliver by reference, not by pasting file content
A product channel has its own constraints. Chat text, email attachments, browser downloads, mobile clients, and webhook consumers differ in size limits, retry behavior, and authorization. A ZeroClaw feature report notes that pasting large generated HTML, spreadsheets, or scripts into a chat message can truncate or corrupt the output and asks for attachment-aware routing.
Build a channel adapter that receives an authorized artifact version, not a local path. The adapter chooses one supported mode:
- attach the immutable version when the channel supports safe attachments;
- send a short-lived product download link when attachment limits are unsuitable;
- deliver a signed webhook reference plus digest for machine consumers;
- show an in-product card with state, size, type, version, expiry, and review status.
Record a delivery attempt separately from publication. A successful database commit does not prove the user received a usable link. The receipt should include destination, version, attempt number, provider message ID when available, expiry, and outcome. Retry delivery with the same published version and idempotency key. Do not republish the bytes merely because a channel timed out.
For multiple outputs, send an ordered manifest or a collection object. Assert that every required artifact is present before claiming complete delivery. Optional artifacts can fail independently, but the response must name the partial result instead of retaining only the last item.
Support edits without overwriting provenance
Users may ask the agent to revise a report, chart, application, or dataset. A Keboola product request describes persistent agent-generated artifacts that need storage, access rights, lifecycle management, and later writeback through the agent.
Treat an edit as a new candidate version. Give the agent a scoped read reference to the authorized current version, capture the revised bytes into quarantine, and run the same validation and release process. Store derived_from_version_id and a change summary, but do not replace the prior object. Existing links can remain pinned to their original version while the product's artifact page points to the newly published current version.
Apply optimistic concurrency to publication. If two revisions start from version 3, the first may publish version 4. The second should receive a conflict and rebase or request review rather than silently becoming version 5 from stale input.
Delete every copy, not only the product record
Artifact deletion is a propagation job. The durable object may have previews, extracted text, thumbnails, search-index entries, cache copies, delivery attachments, provider files, and audit references. Your inventory should classify which copies contain user data, which are immutable evidence, and which must be removed or tombstoned.
A deletion workflow should:
- revoke new reads and signed-link issuance;
- mark the artifact unavailable to agents and users;
- delete or tombstone every derived copy according to policy;
- request provider-side deletion when an API supports it;
- invalidate previews, indexes, and caches;
- verify absence from each required location;
- retain only the minimum deletion evidence allowed by policy.
If a delivery channel copied an attachment outside your control, document that boundary before release. Do not promise deletion your system cannot enforce. Prefer expiring product links when the channel supports them.
Verify the lifecycle with failure tests
A happy-path download test is not enough. Exercise the boundary with cases that match the observed failures:
- the provider container expires halfway through capture;
- the same file event arrives twice;
- the same source file ID returns different bytes;
- artifact part 2 arrives before part 1;
- a stream ends without its final marker;
- a required artifact never arrives;
- review approves digest A, then the candidate changes to digest B;
- the user's access is revoked between capture and publication;
- durable storage succeeds but metadata commit fails;
- publication succeeds but channel delivery times out;
- a channel cannot carry the file's size or type;
- two edits race from the same current version;
- deletion removes the object but leaves a preview or index entry.
For each case, assert artifact state, visible user outcome, allowed retry, and negative evidence. Negative verification matters: a rejected candidate must not appear in durable public storage, an incomplete collection must not be labeled complete, and a deleted artifact must not remain retrievable through an old signed-link path.
Track operational measures such as capture latency, expired-before-capture count, manifest mismatch count, validation rejection reason, review age, publish reconciliation age, delivery success by channel, and deletion verification failures. These metrics describe the artifact system. They do not prove the agent's content is correct, so keep content evaluation separate.
Put the contract in front of every file-producing agent
Start with one product endpoint: registerCandidateArtifact(run_id, source_ref, declared_manifest). Require every runtime and protocol adapter to call it. Make that endpoint return a product artifact ID and state, never a public URL. Add capture, validation, release, delivery, and deletion workers behind the same state machine, then test the failure cases before enabling automatic publication.
The runtime can produce bytes, but only the product can publish an artifact. Enforcing that boundary closes the gap between a successful tool call and a file the right user can safely retrieve, revise, audit, and eventually delete.
References
- A2A protocol specification defines artifact, file-part, streaming-update, append, and task-state semantics.
- OpenAI Code Interpreter documentation documents ephemeral containers and generated-file retrieval.
- Anthropic code execution documentation documents generated-file capture, workspace scope, container expiry, and explicit persistence behavior.
- Microsoft Q&A on Assistant-generated files records a practitioner-reported sandbox-path and product-storage problem.
- IronClaw issue #7541 reports local paths being returned instead of user-accessible attachments.
- ZeroClaw issue #8527 reports truncation risk and requests attachment-aware delivery.
- Google ADK issue #6680 gives a reproducible project report about incomplete streamed artifact state.
- Agents at Scale issue #3180 reports a multi-artifact response being reduced to one item.
- Mastra issue #20092 describes the need for a private review step after execution and before publication.
- Keboola issue #684 requests persistent artifacts, access control, lifecycle management, and later writeback.