Tasks & runs
A task is a durable execution session for a runtime. Every task runs in its own isolated sandbox, so executions never share temporary filesystem or process state. Persistent memory is mounted separately and only when the request resolves to an eligible owner, such as a federated customer member or an end-user identity attached to application traffic.
Tasks come in two shapes:
- Interactive: a live session you send follow-up messages to. Between turns the session can sit
idle, holding its sandbox open and waiting for the next message until an idle timeout tears it down. - One-shot: a single prompt that runs to completion and ends.
A task points to its conversation through
task.metadata.conversation_id. For an agent task the conversation ID is the
task ID: the platform assigns it, and no task-creation surface takes a
conversation identity from the caller.
Sandboxes
When a task starts, Introspection creates an isolated place for Pi to run the pinned recipe: the sandbox. There is exactly one task per sandbox, and the sandbox is destroyed when the task finishes.
This is the foundation of the isolation model. A leaked credential, a runaway process, or a corrupted working directory is bounded to a single task, and each new task starts from a pinned recipe rather than a long-lived machine that drifts over time. Follow-up runs inside one interactive task reuse that task’s sandbox until it closes or times out.
The sandbox’s working filesystem is not the system of record. Uploaded files are materialized into the task workspace before the agent starts, alongside restored task files, recipe files, and memory. Files the agent writes are saved back as durable artifacts, so the outputs survive teardown. The conversation is recorded, and when it completes the recipe’s judges grade it against the exact recipe version and environment that produced it.
A sandbox has scoped, egress-restricted access and holds only a short-lived, task-scoped credential for talking back to the platform. The API key that created the task is never passed into the sandbox, and your real provider credentials are never exposed to the agent — they are injected at the network boundary. See the Security model for details.
Lifecycle
A task moves through a well-defined set of statuses:
| Status | Meaning |
|---|---|
pending | Created, not yet accepted for execution. |
queued | Accepted, waiting for a free concurrency slot in your organization before a sandbox is provisioned. |
scheduled | Assigned to run; sandbox is being provisioned. |
running | The agent is actively working. |
awaiting_user | The agent paused on an interrupt (for example a tool permission) and is waiting for the caller to resume. |
idle | Interactive session is alive and waiting for the next message. |
completed | Reached the end of its lifecycle without an error. Read metadata.completion_reason before treating it as success — see below. |
failed | Ended with an error. metadata.error carries the reason. |
cancelling | A cancel was requested; the task is winding down. |
cancelled | Cancelled and stopped. |
One-shot tasks typically run queued → scheduled → running → completed. Interactive tasks return to idle between turns and pick up again at running when you send a new message.
Diagnosing a task
The task row carries its own diagnosis in metadata. Read the whole row — introspection tasks get <task-id>, or the equivalent SDK call — rather than inferring from status alone. Two metadata keys matter:
completion_reasonexplains why acompletedtask ended. A task that ran its work to the end completes withtask_completion; one torn down by the idle window completes withinactivity_timeout, and one whose session never became usable completes with a reason naming that instead.completedtherefore means “reached the end of its lifecycle”, not “produced a result”.errorcarries the failure reason on afailedtask, including failures that happen before the agent ever runs.
Do not treat status: completed as proof of success. A task that hit the
idle window is completed with metadata.completion_reason = "inactivity_timeout" and may have produced nothing. When you are using a task
as evidence — a deployment smoke test, a verification run — check
completion_reason and inspect the conversation, not just the status.
A task that never started
Some failures happen before the sandbox runs any agent turn. metadata.error describes them in prose rather than as a fixed code, so read it rather than matching against a list. In practice they fall into two groups:
- The sandbox never became usable — it terminated during startup, was not ready in time, or the initial prompt could not be delivered. These are infrastructure failures rather than recipe configuration failures.
- The runtime could not be served as configured — a bring-your-own-key runtime with no applicable LLM endpoint, or an endpoint whose referenced credential cannot be resolved. These name themselves (
byok_no_endpoints,endpoint_credential_missing) and are fixed in bindings, not by retrying.
There is no conversation for these tasks, because a conversation is built from spans the sandbox emits and the sandbox never got that far. Fetching one returns 404. Diagnose them from the task row, and check bindings only when the error points there.
A task that stays queued
queued means the task is waiting for a free concurrency slot in your organization, not that the platform is wedged. Tasks are admitted per organization, so a burst can leave later tasks queued while earlier ones run.
A queued task exits one of three ways: a slot frees and it proceeds, the caller cancels it, or it exceeds the queue-wait budget and the platform cancels it for you. It does not wait indefinitely. Retrying only lengthens the queue.
Your organization’s concurrency limit is derived from your plan, not a setting you change yourself — there is no CLI or dashboard control for it. So the lever you actually hold is running fewer tasks at once: shape the burst, or let the queue drain. If your workload genuinely needs more headroom than the plan allows, that is a conversation with Introspection rather than a configuration change.
A run can also pause for a human in the loop. When the agent hits an interrupt (for example a tool that needs permission), the task moves to awaiting_user and surfaces what it is blocked on: the task’s metadata lists its pending interrupts until they are answered. The caller answers each interrupt as resolved (optionally with a payload) or cancelled, and the task continues. Resolving every pending interrupt returns the task to running, while answering only some keeps it at awaiting_user. Resuming is idempotent — re-sending an already-resolved batch is a no-op rather than an error. See the SDK pages (JavaScript, Python) for the resume API.
Each task gets its own isolated sandbox, and there is exactly one task per
sandbox. An interactive task’s sandbox stays alive across the idle window so
follow-up turns reuse the same filesystem and context.
Task runs
Within a task, a task run is a segment of work opened against a runtime (or against an experiment). A single task can span several runs; each follow-up turn opens a new one.
A run carries a kind that expresses your intent:
| Kind | What it does |
|---|---|
prompt | Opens a fresh turn from a prompt. |
steer | Injects guidance into the turn that’s already in progress. Falls back to prompt if no turn is active. |
Because runs are the unit of streaming and cancellation, you can watch a run’s output live and cancel a run mid-flight without discarding the task.
A run request with no prompt and no kind is a provision call: it asks the platform to prepare the task’s sandbox without opening a turn. It can warm a newly created task or restart one that has already finished (completed or failed) before the next prompt arrives. On a task whose sandbox is already active, it is a no-op that reports the current status.
Warming an interactive task
An interactive application can hide sandbox startup behind the time a user spends composing their first message:
- When the user shows intent to chat, establish the browser session and create a task without a prompt. Creating it is what starts the sandbox, so the warming has already begun here.
- When the user submits, adopt that task, update its placeholder title or metadata, and open the first
promptrun. - If the draft is abandoned, archive the unbound task. Track unfinished cleanup locally so a later page load can retry it.
A bodyless run request is only needed to restart a task that has already finished. On a task created this way it is a no-op that reports status.
// Begin while the user is composing. Creating the task provisions its sandbox,
// so the wait overlaps with typing. `warmup` is application metadata here, not
// a platform switch.
const runner = await client.runtimes("support-agent").run({
identity: { user_id: applicationUser.id },
});
const created = await runner.tasks.create({
agent_name: "agent",
title: "Preparing chat",
idle_timeout_seconds: 600,
metadata: { warmup: true },
});
const taskId = created.task.id;
// Adopt the warm task when the user sends their first message.
await runner.tasks.update(taskId, {
title: "Research the account",
metadata: { warmup: false },
});
const run = await runner.tasks.runs.create(taskId, {
kind: "prompt",
prompt: { text: userMessage },
});Warm only a fresh, unbound draft. An already selected task has its own lifecycle; do not provision it merely because its composer receives focus. Coalesce repeated draft signals and impose a freshness window so typing, focus, and navigation do not create duplicate tasks.
Warmup is an optional latency optimization, not a separate task type. Keep the normal path as a fallback: if creation or provisioning fails, create the task when the user submits and start the prompt normally.
Cancelling a run
Cancellation has two modes:
| Mode | What it does |
|---|---|
abort (default) | Interrupts the in-flight turn immediately and keeps the sandbox warm. The task drops back to idle and the next prompt reuses the same container — what an interactive “stop” button wants. A bodyless cancel aborts. |
drain | Lets the active turn finish and return control, then tears the sandbox down at the next settle boundary. The task lands cancelled and the next prompt cold-boots on a fresh sandbox — the release path. Pass drain_within_seconds to force teardown if the task doesn’t settle in time. |
Draining a task that is awaiting_user keeps its pending interrupts in the task metadata, so the human-in-the-loop question survives the teardown: a later resume rehydrates it on a fresh sandbox instead of losing the exchange.
Both modes are API and SDK surfaces. introspection tasks cancel always sends abort, so it interrupts the turn and leaves the task warm and idle rather than ending it. To drain, call the cancel endpoint with {"mode": "drain"}.
Resumable streams
A run’s live stream can be severed (a network blip, a gateway idle timeout)
without losing anything. Re-attach with the last event id you saw (the SSE
Last-Event-ID header or a ?since= cursor) and the run replays its recent
frames; anything older than the replay window can be hydrated from the task’s
conversation record, deduplicating by item id.
Attaching to a run that has not started yet holds the connection open, emitting
lifecycle frames until the run is live. Pass wait_for_start=0 when you would
rather poll than hold: the server waits for the advertised retry window and
then, if the run has still not begun booting a sandbox, returns 429 with
Retry-After and the run’s current phase in the body. A run that is already
booting holds the connection and emits lifecycle frames regardless, since the
wait is bounded by the boot rather than open-ended. On the task’s active run the
connection is held either way, so a turn is never dropped between attaching and
starting. Back off and retry after a 429.
Work with tasks
The task lifecycle is available through the JavaScript, Python, and Rust SDKs, the CLI, and the REST API. Each surface can create a task, stream its typed AG-UI events, open follow-up runs, cancel active work, and resume interrupts. Start with the JavaScript SDK or CLI task commands once your runtime exists.
Tags on a task
A task carries an optional list of tags: free-form
key:value labels set on create, replaced on update, and usable as a filter
when listing. They group work — one customer, one team, one campaign — and they
also decide access, because a caller whose own member tags intersect a task’s
tags can read and write it. Read Tags before adopting a tag
vocabulary; a label chosen purely for reporting becomes a grant the moment the
same string appears on a member.
Related concepts
- Conversations: the immutable, replayable record of the model calls, tool calls, and messages a task produces.
- Tags: grouping tasks and sharing them across a cohort of members.
- Runtimes: the deployable agent version a task executes.
- State & artifacts: durable files, shares, and owner-scoped memory.
- Experiments: A/B tests that runs can be opened against.
- API Reference: the full task and run API.