Runs
A run is one execution of an agent or task. Use runs for direct messages, follow-ups, and real-time output.
Create a run
Streaming (default and recommended)
Use the context manager so the stream closes cleanly even if you exit early:
from m8tes import M8tes
client = M8tes()
with client.runs.create(agent_id=1, message="summarize this week's support trends") as stream:
for chunk in stream.iter_text():
print(chunk, end="", flush=True)
print(stream.text) # full accumulated outputSee Streaming & Events for event types, text-only streaming, and reconnection, or pass stream=False and poll:
run = client.runs.create(agent_id=1, message="summarize this week's support trends", stream=False)
run = client.runs.poll(run.id)
print(run.output)
teammate_idis the wire name for the agent id: JSON request and response bodies keep it. The SDK acceptsagent_id(canonical) andteammate_idalike.
Run options
All run options
Replies inherit task_setup_tools and feedback unless overridden.
Follow up on a run
A follow-up continues the same run: it re-opens the run, keeps the prior context, and does not consume a new run-count slot (it reuses the original run id and only burns tokens).
first = client.runs.create_and_wait(agent_id=1, message="find all open incidents")
with client.runs.reply(first.id, message="now draft an update for each owner") as stream:
for chunk in stream.iter_text():
print(chunk, end="", flush=True)runs.reply() inherits the run's settings: permission mode keeps applying, and AskUserQuestion stays enabled on runs created with human_in_the_loop: true. Pass human_in_the_loop: false on the reply to pin non-interactive behavior.
Replying while the run is still executing
A reply works on a live run too. If the run's current turn is still executing, the message is queued and delivered as the run's next turn the moment the current one ends. Never a second concurrent execution. The response tells you which happened:
run = client.runs.reply(42, message="also check the staging env", stream=False)
if run.delivery == "queued": # run was mid-turn; message delivers next
print("queued as message", run.queued_message_id)
else: # "resumed": it became the next turn now
print("resumed")With stream=True on a queued reply, the SDK joins the live run's ongoing stream (the queued message itself delivers on the next turn). Delivery normally starts within seconds of the turn ending; across a backend deploy it can take up to ~15 minutes (the recovery sweep).
Every queued message resolves observably: run.message_received when queued, then either run.started for its delivery turn or run.message_cancelled with a reason (owner cancelled [agent senders only; your own replies to a run you cancelled still deliver and revive it], sandbox expired, account disabled, or 24h undelivered).
While the run is live, replies with file attachments or per-reply overrides (tools, permission_mode, task_setup_tools, feedback, human_in_the_loop) return 409. Overrides only apply to an immediate resume; silently dropping them would be worse.
This is also how agent-to-agent messaging behaves: when one of your agents messages another agent's run, the message arrives the same way. Queued if the target is busy, delivered as the next turn, and always attributed to the sending agent, never to you.
Structured output
Pass a JSON Schema as output_schema and the run returns typed data on output_data, so you can act on fields instead of parsing prose.
run = client.runs.create_and_wait(
agent_id=1,
message="Review this Sentry issue and rate its severity.",
output_schema={"type": "object", "properties": {"severity": {"type": "string", "enum": ["low", "medium", "high"]}}},
)
if run.output_data: # always None-check
print(run.output_data["severity"]) # -> "high"Schema rules and gotchas
- The schema root must be
"type": "object", sooutput_datais always an object. - Inline your definitions.
$refand$defsare rejected. Pydantic'smodel_json_schema()and zod both emit$defsfor nested models, so flatten before sending. output_datacan benullon a completed run. A run cut short by truncation, a pause, or a spend limit still completes, with its textoutputintact but no structured result. Always null-check.- The schema sticks to the run: replies, resumes, and retries stay structured without re-sending it.
run.completedwebhooks carryoutput_datatoo.
Run outcome
GET /runs/{id}/outcome returns the condensed result of a run in one call (the agent's closing message, the structured result, and what the run cost) instead of the full transcript.
outcome = client.runs.outcome(run_id=42)
print(outcome.summary, outcome.cost_usd) # "Audit done: paused 3 wasteful keywords." "0.4831"
if outcome.needs_reply: # the agent is asking for a decision
client.runs.reply(42, message="Approve both")
if outcome.delivery_channel == "none": # agent chose silence. No completion email
passOutcome fields
Scheduled delivery (set_run_delivery)
On scheduled and webhook-triggered runs, the agent chooses how the outcome reaches the owner by calling the set_run_delivery built-in tool before its closing message:
Read the choice from runs.outcome().delivery_channel or run.completed webhook data.delivery_channel. Disable the tool with disabled_builtin_tools: ["delivery"] on the agent.
Embed agents (prompt_profile: "bare") do not receive m8tes's delivery playbook in the system prompt. Document set_run_delivery in your agent instructions if scheduled runs should use it.
run_id = 1
outcome = client.runs.outcome(run_id)
if outcome.delivery_channel == "email" and outcome.needs_reply:
notify_user(outcome.summary, subject=outcome.headline)
elif outcome.delivery_channel == "none":
logger.info("scheduled run chose silence run_id=%s", run_id)Legacy NEEDS-REPLY / HEADLINE: marker lines in closing prose are deprecated; delivery_channel, needs_reply, and headline on outcome and webhooks are authoritative.
run.status values: running, paused, awaiting_approval (waiting for user input or approval), completed, failed, cancelled, closed, archived.
Execution environment
Tools run in an isolated, per-account Linux sandbox. m8tes provisions it automatically.
See Computer use for desktop tools.
List runs
sort="priority" puts runs waiting on a human first; the default is newest first.
for run in client.runs.list(sort="priority", limit=20).auto_paging_iter():
print(run.id, run.status)Run files
Attach input files with files= on runs.create (see Files). Runs can also generate files; list and download them after execution:
for f in client.runs.list_files(run_id=1):
print(f.name, f.size)
content = client.runs.download_file(run_id=1, filename="report.csv")Next: Agents · Tasks · Streaming & Events · Webhook Events