Runs

A run is one execution of an agent or task. Use runs for direct messages, follow-ups, and real-time output.

Run list and detail responses include needs_reply when a completed turn asks for a concrete replyable decision (approve, answer, connect). Show these runs as Needs you, even after they have been viewed or emailed. Soft FYI / out-of-reach notes must not set needs_reply. Agents choose delivery when awaiting: Slack for one quick decision, email for deep/batched asks, or in-app only (channel=none) when the user is already in the run — unattended runs (schedule, webhook, Day-1) must interrupt via Slack or email (none + needs_reply is rejected there). Send the answer with runs.reply(); viewing the run does not answer the ask. On the Platform board, Archive clears a reply-request run from Needs you in one tap (Undo during the exit animation). A newer completed scheduled run of the same task (and user_id) supersedes it, so recurring tasks do not stack stale asks. closing_preview is a short headline for cards; latest_message_preview contains up to 600 characters from the latest assistant message for hover previews.

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

For a failed or cancelled run, can_continue indicates that it has a saved, unexpired conversation eligible for runs.reply(). Prefer Continue when this is true; a run that never started needs its original task retried. Reply still checks current funding and permissions.

For an explicit continuation, pass reset_auto_recovery=True in Python or { reset_auto_recovery: true } in TypeScript. This grants a fresh three-attempt recovery budget when the reply is accepted, including when it is queued. Automatic loops should leave this option false. auto_recovery_attempt_count reports attempts in the current budget; auto_retry_count preserves the cumulative history. A later platform update can then recover work you explicitly continued, even if an earlier recovery budget was exhausted.

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 platform update 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?