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

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 output

See 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_id is the wire name for the agent id: JSON request and response bodies keep it. The SDK accepts agent_id (canonical) and teammate_id alike.

Run options

FieldTypeDefaultDescription
messagestring (required)n/aPrompt or instruction for the run
teammate_idintn/aExisting agent to execute
permission_modestringagent defaultOverride with autonomous, approval, or plan. See Human-in-the-Loop
user_idstringn/aEnd-user scope. See Users
output_schemaobjectn/aJSON Schema for a typed result. See Structured output
All run options
FieldTypeDefaultDescription
streambooltrueStream events in real-time
toolsstring[]n/aOverride agent tools for this run
fileslistn/aInput files to upload. See Files
memorybooltrueInclude saved per-user memory
historybooltrueInclude prior run context
human_in_the_loopboolinheritedOmit to inherit false for autonomous and true for approval/plan
task_setup_toolsbooltrueEnable the internal same-scope agent, task, run, webhook, inbox, and app-management tools
feedbackbooltrueEnable the internal issue-reporting tool (report_issue)
email_inboxboolfalseEnable email inbox on the auto-created agent (only when no teammate_id is given). The response includes email_address; emails there trigger future runs

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:

Python
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.

Python
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", so output_data is always an object.
  • Inline your definitions. $ref and $defs are rejected. Pydantic's model_json_schema() and zod both emit $defs for nested models, so flatten before sending.
  • output_data can be null on a completed run. A run cut short by truncation, a pause, or a spend limit still completes, with its text output intact but no structured result. Always null-check.
  • The schema sticks to the run: replies, resumes, and retries stay structured without re-sending it.
  • run.completed webhooks carry output_data too.

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
    pass
Outcome fields
FieldMeaning
summaryThe agent's closing message. None when the run ended on a tool call
headlineOne-line outcome descriptor supplied by the agent, when present
needs_replyTrue when the closing message asks for a decision. Answer via runs.reply()
needs_reply_countWhen several decisions are waiting, how many (may be null)
delivery_channelemail, slack, or none. Set by the agent via set_run_delivery during the run
output_dataStructured result matching the run's output_schema
cost_usdMetered cost as a decimal string. None until cost is recorded
message_count, input_tokens, output_tokens, total_tokensRun-level usage metrics

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:

channelMeaningCompletion email
emailDeep work (report, audit). Short closing hook; depth in latest-report PDFSent when the task has email_notifications: true
slackQuick 1–3 line nudgeSuppressed (first-party only: agent also Slacks via send_slack_message_to_user)
noneAll clear. Nothing worth interruptingSuppressed

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.

Python
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.

StateWhat to expect
sandbox-connectingFirst run boots the environment; allow a few to tens of seconds
Warm sessionLater runs reuse the sandbox
429 SANDBOX_CONCURRENCY_LIMITWait for capacity, then retry with backoff

See Computer use for desktop tools.

List runs

sort="priority" puts runs waiting on a human first; the default is newest first.

Python
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:

Python
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

Was this page helpful?