Files
Agents generate files during runs (CSV, JSON, Markdown, HTML, code, plain text). Instruct the agent to write them, then download through the API.
Generate files
Tell the agent what to produce in the task instructions, then poll for completion:
from m8tes import M8tes
client = M8tes()
run = client.runs.create(
agent_id=1,
message="Analyze our open support tickets and write a CSV summary to tickets_report.csv",
stream=False,
)
run = client.runs.poll(run.id)Upload files to a run
Attach files for the agent to work on: a CSV to analyze, a PDF to summarize. Pass files= to runs.create (a path, a (filename, bytes) tuple, or an open binary file); every other argument works the same:
run = client.runs.create(
agent_id=1,
message="Summarize the Q3 numbers in this spreadsheet",
files=["q3-report.csv"],
stream=False,
)The payload form field carries the same JSON as POST /runs (teammate_id is the wire name for the agent id; the SDK accepts agent_id alike). Uploaded files land in the run's working directory and their names are appended to the prompt, so the agent finds them without extra instructions. Limits: 20 files, 100MB per file, 500MB per request.
The Platform picker uses the same size limits and supported formats as the API: images (JPEG, PNG, GIF, WebP), PDF, text and code, CSV, XML/YAML, ZIP/GZIP/TAR, and Office documents (DOCX, XLS/XLSX, PPT/PPTX). Missing or generic binary MIME types are inferred from supported filename extensions; explicit unsupported MIME types are rejected. The browser MIME alias video/mp2t is treated as TypeScript only for .ts files. JavaScript accepts both application/javascript and text/javascript.
Attachments are also recorded on each user message: client.runs.messages(run.id) returns event_metadata["attachments"], a list of { "name": "q3-report.csv", "size": 1234 } entries. Names are the sanitized filenames in the sandbox; sizes are bytes. Follow-ups record only their new attachments. Join-stream user_message events carry the same list as attachments. Older messages may not have this metadata.
Keep and share an output
Run files live in the run's sandbox and latest-report is rewritten on the next run. Promote the ones worth keeping into an artifact: a durable copy linked to the run, task, and agent, versioned per agent + name (so a weekly report is v1, v2, v3…), and shareable with a public link — the same model as sharing a run.
artifact = client.artifacts.create(run_id=run.id, filename="latest-report.md")
print(artifact.version, artifact.size_bytes)
share = client.artifacts.share(artifact.id)
print(share.share_url) # anyone with the link can open it — revoke with client.artifacts.unshare(artifact.id)
report = client.artifacts.download(artifact.id) # raw bytes, authenticatedA run's latest-report.md is promoted automatically when the run finishes (and again on each follow-up turn, keeping the same artifact current), so client.artifacts.list(agent_id=1) is the report history for that agent. Artifacts are capped at 10 MB (a larger file is refused with a validation_error); uploaded input files are never promotable; promotion is idempotent per run + filename.
List and download files
files = client.runs.list_files(run.id)
for f in files:
print(f.name, f.size)
content = client.runs.download_file(run.id, filename="tickets_report.csv")
with open("tickets_report.csv", "wb") as fh:
fh.write(content)Need live progress while files are written? See the streaming tracker below; otherwise just call list_files after the run completes.
Advanced: track file writes via streaming
Tool arguments stream as JSON fragments on tool-call-delta events; tool-call-start carries only tool_name and tool_call_id. To see filenames in real time, accumulate the deltas for each Write call and parse them on tool-call-end:
import json
write_args = {} # tool_call_id -> accumulated JSON arguments
with client.runs.create(
agent_id=1,
message="""Generate a weekly performance report:
1. Write an executive summary to weekly_summary.md
2. Export raw metrics to metrics.csv
3. Save top performers to top_10.json""",
) as stream:
for event in stream:
if event.type == "text-delta":
print(event.delta, end="", flush=True)
elif event.type == "tool-call-start" and event.tool_name == "Write":
write_args[event.tool_call_id] = ""
elif event.type == "tool-call-delta" and event.tool_call_id in write_args:
write_args[event.tool_call_id] += event.delta
elif event.type == "tool-call-end" and event.tool_call_id in write_args:
path = json.loads(write_args.pop(event.tool_call_id)).get("file_path", "")
print(f"\n writing {path.split('/')[-1]}...")
run_id = stream.run_id
# Download everything the run wrote
for f in client.runs.list_files(run_id):
with open(f.name, "wb") as fh:
fh.write(client.runs.download_file(run_id, f.name))Example: weekly report pipeline
Combine file generation with scheduling for recurring reports:
reporter = client.agents.create(
name="weekly reporter",
tools=["stripe", "googlesheets", "gmail"],
instructions=(
"Generate weekly reports. Write a markdown summary to weekly_report.md "
"and a CSV of raw data to weekly_data.csv. Email the summary to the team."
),
)
task = client.tasks.create(
agent_id=reporter.id,
instructions=(
"Pull last week's key metrics from Stripe and Google Sheets. "
"Write weekly_report.md and weekly_data.csv. "
"Email weekly_report.md to team@acme.com."
),
schedule="0 9 * * 1",
schedule_timezone="America/New_York",
)
run = client.tasks.run_and_wait(task.id)
for f in client.runs.list_files(run.id):
print(f.name, f.size)See examples/file-report.py for the full pipeline: streaming progress, scheduled generation, and download.
Tips: reliable file output
Be specific in your task instructions. Name the file, the format, and the columns:
run = client.runs.create(
agent_id=1,
message="Query open tickets, then write a CSV with columns: id, subject, priority. Save as tickets.csv",
stream=False,
)For multiple files, number them in one message ("1. Write a summary to report.md, 2. Export metrics.csv, ...") as in the streaming example above.
Next: Runs · Streaming & Events · Scheduling