Walk the Managed Agents REST API end to end with curl — create an Agent and Environment, upload and mount a dataset, drive a Session through data profiling, cleaning, and conclusions, retrieve the artifacts, and archive the resources.
Overview
This tutorial walks the Managed Agents REST API with curl: create an Agent and an Environment, upload and mount an order log seeded with dirty data, open a Session and let the agent work through "profile → clean → compute → report" on its own, then retrieve the artifacts and archive the resources.
The data analysis scenario is chosen because it carries a natural feedback loop: the agent must discover what is wrong with the data before it can compute the right metrics, and after computing it has to go back and verify the numbers are self-consistent. The example covers the API shapes every later tutorial builds on: Agent / Environment / Session, file mounting, the event stream, artifact retrieval, and archiving.
This page focuses on the API mechanics themselves and gets the pipeline running with minimal dependencies; the artifacts are Markdown + CSV. Quality-oriented advanced topics (narrative HTML reports, interactive charts, more refined system prompts) can be layered on top of this pipeline.
Before starting, agree on the environment variables used by every curl example below:
AGENT_ID, ENV_ID, SESSION_ID, and FILE_ID.
Three resource concepts
Managed Agents has three core resources:
- Agent: a reusable, versionable configuration containing
model,system(the system prompt), andtools. Each update automatically bumpsversion. - Environment: the container template (sandbox), specifying
packagesandnetworking. - Session: binds an Agent to an Environment, mounts files, and produces the event stream. Creating a session locks a full snapshot of the agent's latest version.
Step 1: Create the Agent
The output quality of a data analysis agent depends mostly on the system prompt. Instead of writing a "how to" procedure, write working discipline — verify the data before drawing conclusions, clean conservatively, every conclusion carries a number. Let the agent figure out the rest.
Use the built-in toolkit with type: "builtin_toolkit" (an agent takes at most one). This example enables the six core tools — bash, read, write, edit, glob, grep — enough for an all-offline analysis; the toolkit also offers web_search and web_fetch for web access, not needed here. mark_artifacts is injected automatically by the platform runtime to register artifact files — no need to declare it (listing it explicitly changes nothing). download_file is retired — do not configure it; retrieve artifacts through the Files API (see Step 6).
Choose qwen3.8-max as the model. Managed Agents accepts model IDs of the qwen3.8-max / qwen3.7-max / qwen3.7-plus / qwen3.6-plus / qwen3.6-flash generation; legacy DashScope IDs like qwen3-max are rejected outright (400 AGENT_010 model does not exist). The authoritative list is the console dropdown.
The system prompt contains newlines; putting it directly inside shell single quotes is error-prone. Write it to a file first and use -d @:
id from the response (shaped like agent_<ULID>); the response also echoes version: 1:
- "Run a data quality check first": without it, the model tends to
reada glance at the data and start reporting numbers, silently carrying dirty data into the conclusions. - "Data that looks extreme but is internally consistent… do not delete": this is the guardrail against over-cleaning. The dataset plants one wholesale order with a huge but fully consistent amount — over half of total revenue. Without this line the model is likely to drop it as an outlier and report a "prettier" but wrong picture.
builtin_toolkit has two levels of switches — default_config.{enabled, permission_policy} and configs[ ].{name, enabled, permission_policy}. permission_policy accepts {"type": "always_allow"} (execute directly) or {"type": "always_ask"} (wait for confirmation on every call). When a tool call needs human approval, the SSE stream surfaces stop_reason.type == "requires_action" (carrying pending_batch_id and pending_call_ids); reply with a tool_approval_response event referencing the batch_id + call_id to approve. See Session Event Stream. This example uses always_allow throughout.Updates use POST + version
To adjust tools or the prompt later, the update endpoint is POST /agents/{agent_id} (PATCH returns 405 method not allowed), and the body must carry the current version as an optimistic lock, otherwise 400 AGENT_010 invalid agent parameters - version: version must not be empty. A successful update bumps version by 1:
Step 2: Create the Environment
Set config.type to cloud to use the managed sandbox (immutable after creation). Data analysis needs pandas — declare it in config.packages.pip and it is already installed when the session's container starts; the agent spends no turn running pip install:
id (shaped like env_<...>):
config.packages (plus an extra "type": "packages" field). To confirm preinstallation actually worked, have the agent print the version in the Step 5 task — pandas is ready in the sandbox and no pip install ever runs. For the full field reference, see Cloud Hosted Environment; an environment with pre-installed packages may need warm-up time after creation, and session creation is rejected until it completes — see "Environment warm-up" on that page.
The environment's network policy currently takes
{"type": "unrestricted"} (all egress allowed). This example logically needs no network — packages are preinstalled and the analysis is offline throughout. For egress behavior details (gateway MITM, TLS validation), see Advanced: Credential Vault Security and the Egress Gateway.Step 3: Upload the dataset
Prepare a 26-row order log seeded with three kinds of dirty data and one "trap":
| # | Problem | Where | Correct handling |
|---|---|---|---|
| 1 | Duplicate row | 1014 appears twice | Drop the copy, 26 rows → 25 |
| 2 | Missing amount | 1006, 1019 | Backfill with unit_price × quantity (160, 480) |
| 3 | Inconsistent region spelling | east, north, East (with leading space) | strip + case folding; 6 spellings merge into 3 regions |
| 4 | Trap: extreme value | 1025 (1200 × 20 = 24000) | Keep it. Amount is consistent with unit price × quantity — a real wholesale order |
east and East count as two regions, splitting revenue and corrupting the regional ranking. Problem 4 tests whether the agent over-cleans — it is 52.68% of total revenue, and deleting it changes the whole picture.
The correct answers (after cleaning) — keep these to check the agent in Step 6:
- 25 rows, region has only
East/North/South; - total revenue 45,560;
- by region: East
37,590> North4,400> South3,570; - by category: Electronics
38,700> Home3,900> Apparel2,960; - largest order:
1025at24,000, 52.68% of total revenue.
Upload
Save it as orders.txt and upload (files max 10 MB). Mind the extension: the current type check rejects .csv (11900014 file type not allowed); uploading the same content with a .txt extension works fine. mount_path decides the file name inside the sandbox, and the agent parses comma-separated content as usual:
id:
status starts as checking; poll GET /files/{id} until it becomes available (other values: rejected / type_rejected):
checking to available in under 10 seconds. Uploads pass through a security scan — do not create the session right after uploading. For file types, size limits, and console operations, see File Upload and Mounting.
Step 4: Create the Session
The session binds the agent and environment, mounts files, and starts a fresh container. resources places the data into the container before the agent starts.
Two hard constraints on mount paths:
resources[ ].mount_pathmust start with/uploads/.- The file's actual path inside the sandbox is
/mnt/session+mount_path, i.e.mount_path: "/uploads/orders.csv"→ actual path/mnt/session/uploads/orders.csv, read-only.
mount_path decides the file name inside the sandbox, independent of the uploaded file name. Because the mount is read-only, the agent must copy files to a writable directory (such as /mnt/user or /tmp) before working on them; artifacts meant for retrieval go to /mnt/session/outputs/.
Pass the agent as a plain ID string (the session locks a full snapshot of that agent's latest version; the first input is delivered after creation via a message event — see Step 5):
id (shaped like sesn_<ULID>); the response also carries status (idle / running / terminated):
The
resources[ ].file_id in the response is a new ID — the platform copies your file into session scope; the original file is untouched.Step 5: Drive the agent and observe
Two moves: post a message event carrying the task, then read the event stream until the turn ends.
Session creation takes no initial input, so driving the agent is a two-step dance — create the session, then POST one message event. (For "trigger with the first message attached", use a Deployment — it supports initial_events, delivered at trigger time.)
The event stream is one SSE connection (better than polling for watching the agent iterate; this example runs about 2–3 minutes). Two key points:
- Open the stream before sending the message. Establish the SSE connection first, then POST the
message, to guarantee observability; sending first risks losing events to a race. - Exit when the session is
idleandstop_reason.type == "end_turn". The session isidlewhenever it waits for input (turn finished, or a self-hosted tool result needs backfilling) — onlyend_turnis a true exit signal.
Open the SSE stream first
The SSE endpoint requires the header Accept: text/event-stream:
:connected comment line, then events arrive in groups of four lines (note: no space after the colon):
data: line — a JSON Message object. The event: line is always message (it is the SSE frame type, not the event type); the actual event type is in data's type field.
Besides :connected, the stream interleaves two comment lines: :HTTP_STATUS/200 before each event, and :keepalive every 30 seconds when idle. Skip every line starting with : when parsing.
GET /sessions/{id}/events/stream is the streaming endpoint. Requesting GET /sessions/{id}/events (without /stream) with an SSE Accept header does not upgrade; it returns the one-shot historical event JSON as usual.Then send the task message
POST the message event from another terminal. The paths in the task text must use the actual sandbox paths /mnt/session/uploads/...:
type is message; the server echo and the stream events use the same type, with role distinguishing who spoke (user is the echo, assistant is the agent's output).
How to read the event stream
Common fields of the Message object in data:: type, status, id, created_at, sequence_number, role, thread_id, content[ ].
Two easy mistakes:
- The top-level
statusis not the session state. It is the status of the message itself (usuallycompleted). The session state hides insidecontent[0].data.session_status. thread_idis top-level, not undermetadata.
type values you will actually see:
type | Meaning |
|---|---|
message | Message text. role: user is the echo of what you sent, role: assistant is agent output; text in content[ ].text |
reasoning | Thinking placeholder event — carries no content field (parse defensively; do not index d["content"] directly) |
tool_call | Agent invokes a tool; content[0].data = { name, arguments, call_id } |
tool_call_output | Tool result; content[0].data = { name, call_id, output }, where output is a JSON string |
model_request_start / model_request_end | Start/end of each model request; _end's content[0].data carries input_tokens / output_tokens / cache hits — good for cost accounting |
session_status | Session state change, see below |
session_status payload lives in content[0].data:
- Running:
{ "session_status": "running" } - Turn finished:
{ "session_status": "idle", "stop_reason": { "type": "end_turn" } }→ break - Session terminated:
{ "session_status": "terminated" }→ break - Tool result needed:
{ "session_status": "idle", "stop_reason": { "type": "requires_action" } }→ sendtool_approval_responseorfunction_call_output; do not exit
A typical turn's trajectory
A turn runs about 110–185 seconds. One full trajectory (the agent's exact moves vary run to run, but the skeleton is stable):
bashprints the version, confirming pandas is preinstalled;bashmkdir -p /mnt/user /mnt/session/outputs, then copies the csv from the read-only mount into/mnt/user;writea profiling script →bashruns it;- Recovers from a problem on its own: the script gets named
inspect.py, colliding with the standard library, so numpy's internalimport inspectpicks up the script and throws a traceback. The agent reads the error,mvs it tohealth_check.py, and re-runs cleanly; - profiling output:
(26, 7), 1 fully duplicated row, 2 missingamountvalues, 6 region spellings; writea clean+analyze script →bashruns it. The script includes a built-in check: rows whereamount != unit_price × quantitycount0;writewritesfindings.md;- Self-review: runs a
bash python3 - <<EOFto re-read the output CSV and recompute; mark_artifactsregisters both artifacts in one call, returning eachfile_id;- assistant summary →
end_turn.
inspect.py would collide, and nobody looked up the pandas API for it — it discovered the problem from the error and re-ran after fixing. Those are exactly where "iteration" happens. Which pit gets stepped on varies by run, but the loop "run → read result → fix → re-run" is stable, and the final numbers match across runs.
Deduplication and reassembly: the SSE stream currently offers no cursor or resume token; after a reconnect, deduplicate by event
id. Streaming assistant text is theoretically accumulated by sequence_number, but that field is often null and text arrives as whole completed messages — parse as "accumulate if sequence_number exists, otherwise treat as a complete message".Step 6: Retrieve the artifacts and verify
When the agent calls mark_artifacts, the return value already carries each artifact's file_id:
scope_type + scope_id — not the nested scope[type] form, which is silently ignored and returns the whole workspace's files):
/mnt/session/outputs/ appears in this list with downloadable: true — whether or not it was registered via mark_artifacts. The dividing line is the directory, not the registration: your uploaded inputs (mounted under /mnt/session/uploads/) are downloadable: false and cannot come back down. mark_artifacts' real benefit is getting the file_id immediately plus a description — no waiting for the scan, no guessing from a list later.
Download via the /content subpath (there is no /download endpoint — it 404s):
Note: the agent often writes
to_csv(..., encoding="utf-8-sig"); the retrieved CSV then has a BOM, and csv.DictReader sees the first column name as \ufefforder_id instead of order_id. To index by order_id, open with open(..., encoding="utf-8-sig").1025; it calls it out separately and adds a robustness note (wording varies, but "keep + flag" is the stable behavior):
Order 1025 alone contributes over half of revenue… excluding it: East revenue is 13,590, still first; Electronics revenue is 14,700, still first. That is, both rankings are insensitive to this extreme order, but percentages like "East at 82.51%" and "Electronics at 84.94%" depend heavily on it and should be flagged when quoted.That is exactly what the Step 1 line "extreme but internally consistent data is real business data" bought.
Check the usage of this run
GET /sessions/{id} returns stats and usage — check the cost after the run:
stats: { "active_seconds": 113.7, "duration_seconds": 139.6 }; usage: { "input_tokens": 118011, "output_tokens": 4431, "cache_read_input_tokens": 104152, "cache_creation_input_tokens": 13793 }. Runs land in the 110–185 s / 115k–171k input token range depending on how much the agent reworked. 82%–88% of input is cache hits — multi-turn tool calls carry the same context repeatedly, so cache hit rate matters a lot for cost. See Billing for pricing.
Step 7: Clean up (archive)
Use archive to close out Session / Environment / Agent — it tears down the container, stops metering, and hides them from default lists, but keeps records, configuration, and event history for audit. Archive in order Session → Environment → Agent:
status becomes terminated; the environment and agent get an archived_at timestamp and disappear from default list endpoints.
In practice, agents and environments are worth keeping for reuse — next time new data arrives, just POST /sessions with a new session mounting it; no rebuild needed.
Key takeaways
- Agent / Environment / Session, three decoupled layers: the agent is the versionable "persona + tools", the environment is the reusable "sandbox template", the session is one self-contained run. The first two are created once and reused.
- Install dependencies in the environment, not in the turn: packages declared in
config.packages.pipare ready at container start; the agent spends no tokens or time onpip install. - Enable tools one by one:
default_config.enabled: trueis only the toolkit master switch; what the model actually receives is decided byenabled: trueentries inconfigs[ ]. Skip that and the agent has onlymark_artifacts. - Files are read-only mounts plus a writable workspace: uploads sit at
/mnt/session/uploads/(read-only); the agent must copy to/mnt/useror/tmpto modify, and writes artifacts to/mnt/session/outputs/./mnt/userand/mnt/session/outputsmay not exist in a fresh container —mkdir -pfirst. - Artifacts leave via
/mnt/session/outputs/: files written there are scanned by the platform and appear in the session-scoped file list after the turn, downloadable.mark_artifacts' value is getting thefile_idimmediately with a description — no scan wait, no list digging, and one call registers multiple artifacts. - Drive with events, observe with SSE: a
messageevent moves the agent intorunning; the SSE stream returns assistant text and tool calls in real time; usesession_status+stop_reasonto decide when to stop. - Disambiguate
idle: the session stateidleoccurs both on "turn finished (end_turn)" and "tool result needed (requires_action)" — distinguish bystop_reason.type; onlyend_turnexits. - Guardrails belong in the system prompt: "profile before concluding" and "do not delete extreme but self-consistent data" decide this example's success — they beat any procedural description.
Appendix: polling instead of streaming
For shorter tasks, or production code that cannot hold a long connection, poll the historical events endpoint GET /sessions/{session_id}/events instead. After sending the message, pull events every 2 seconds and inspect the last one:
data[0] has the same Message structure as the SSE stream, so the exit check is identical — look at the session_status-typed item's content[0].data:
session_status is terminated, or idle with stop_reason.type == end_turn → done.
sequence_number in historical events is often null; combine order (asc/desc) with created_at[gt|gte|lt|lte] for time filtering, and page through with next_page.
Trade-offs:
- Streaming (SSE): best for watching the agent work live; the cost is holding a long connection (the process must stay alive; a network drop loses the stream).
- Polling: stateless, survives process restarts, composes well with queues and cron; the cost is latency and no live progress.
requires_action stop_reason" or "Deployment (cron / manual /run) + query runs afterwards"; see Advanced: Productionize a Morning-Briefing Robot. For platform-side push (Webhooks), see Advanced: Webhook Event Notifications.
Cheat sheet
Model IDs: qwen3.8-max / qwen3.7-max / qwen3.7-plus / qwen3.6-plus / qwen3.6-flash.
Built-in tools: core tools bash, read, write, edit, glob, grep plus web tools web_search, web_fetch — each must be enabled: true in configs[ ]. The platform additionally auto-injects mark_artifacts (no declaration needed). download_file is retired; do not configure it.
File upload: max 10 MB; binary files must reach status: available (from checking) before mounting.
File paths:
- Upload mount point:
/mnt/session+mount_path(mount_pathmust start with/uploads/, read-only).mount_pathdecides the file name inside the sandbox. - Writable scratch area:
/mnt/useror/tmp. - Retrievable artifacts:
/mnt/session/outputs/— written files are scanned and downloadable (no registration needed);mark_artifactsgets thefile_idimmediately with a description.
| Item | Correct form |
|---|---|
| Update agent | POST /agents/{id} (not PATCH); body must carry current version and name |
| SSE stream | GET /sessions/{id}/events/stream (without /stream it does not upgrade) |
| Message type in events | message (role distinguishes user echo / assistant output); thinking events are reasoning. The SSE frame's event: line is always message — not the event type |
| Session state location | content[0].data.session_status (top-level status is the message status, always completed) |
| Thread ID location | top-level thread_id (not under metadata) |
| List files by session | ?scope_type=session&scope_id=sesn_... (nested scope[type] is ignored) |
| Download a file | GET /files/{id}/content (/download is 404) |
idle: the session state idle occurs on both end_turn and "tool result needed" (requires_action); distinguish with stop_reason.type.
Next steps
- Advanced: Productionize a Morning-Briefing Robot: MCP tools, the vault, and scheduled deployment triggers.
- Advanced: Prompt Versioning and Rollback: put this agent under evaluation and canary release.
- Advanced: Multi-Agent Custom Proposals: orchestrate specialist agents with a coordinator.