Skip to main content

Getting Started: Build a Data Analysis Agent

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:
# Your workspace's Base URL (cn-beijing region only at the moment); see the API overview
export AGENTSTUDIO_URL="https://{workspace_id}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio"
# DashScope API Key — one key covers all resources in the workspace
export DASHSCOPE_API_KEY="sk-xxxxxxxx"
For authentication and how to find your Base URL, see API overview. As the flow proceeds, you will also export 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), and tools. Each update automatically bumps version.
  • Environment: the container template (sandbox), specifying packages and networking.
  • 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.
Agent and Environment are created once and reused across many sessions. Each session is one self-contained run.

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).
The most common configuration mistake: default_config.enabled: true does not actually deliver the tools to the model. You must list every tool you want in configs[ ], each explicitly set to enabled: true. With only default_config.enabled: true and no configs, the agent's tool list contains nothing but the auto-injected mark_artifacts; any bash call then returns TOOL_NOT_FOUND: Tool 'bash' not found. Available tools: ['mark_artifacts'] and the turn ends with an apology about having no tools.
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 @:
cat > agent.json <<'EOF'
{
  "name": "data-analyst-intro",
  "model": { "id": "qwen3.8-max" },
  "system": "你是一名数据分析师。你的工作方式是「先验数据,再下结论」:\n\n- 一律写 python3 脚本处理数据,不要凭眼睛看表格就下结论。\n- 动手算指标之前,先做一次数据质量体检:行数、重复行、缺失值、以及每个分类字段的实际取值。\n- 清洗要保守。只修能够确证的问题:完全重复的行、可由其他列推算出的缺失值、同义但写法不一致的取值。\n- 看起来极端但内部自洽的数据是真实业务数据,要在报告里点出来,不要删掉。\n- 每个结论都必须带具体数字。\n\n产物写到 /mnt/session/outputs/ 下,全部写完后用 mark_artifacts 一次性登记。",
  "tools": [
    {
      "type": "builtin_toolkit",
      "default_config": {
        "enabled": true,
        "permission_policy": { "type": "always_allow" }
      },
      "configs": [
        { "name": "bash",  "enabled": true },
        { "name": "read",  "enabled": true },
        { "name": "write", "enabled": true },
        { "name": "edit",  "enabled": true },
        { "name": "glob",  "enabled": true },
        { "name": "grep",  "enabled": true },
        { "name": "mark_artifacts", "enabled": true }
      ]
    }
  ]
}
EOF

curl -X POST "$AGENTSTUDIO_URL/agents" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -H "Content-Type: application/json" \
  -d @agent.json
Take the id from the response (shaped like agent_<ULID>); the response also echoes version: 1:
export AGENT_ID="agent_xxxxxxxx"
Two lines in that prompt are the crux of this example:
  • "Run a data quality check first": without it, the model tends to read a 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:
curl -X POST "$AGENTSTUDIO_URL/agents/$AGENT_ID" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "version": 1, "name": "data-analyst-intro", "model": { "id": "qwen3.8-max" }, "system": "...", "tools": [ ... ] }'
Sessions lock the agent version snapshot at creation — existing sessions never pick up a new version; after editing the agent, create a new session. For the full versioning workflow (evaluation, canary, rollback), see Advanced: Prompt Versioning and Rollback.

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:
curl -X POST "$AGENTSTUDIO_URL/environments" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "data-analyst-env-01",
    "description": "Intro data analysis environment: pandas preinstalled",
    "config": {
      "type": "cloud",
      "packages": { "apt": [ ], "pip": ["pandas"], "npm": [ ] },
      "networking": { "type": "unrestricted" }
    }
  }'
Take the id (shaped like env_<...>):
export ENV_ID="env_xxxxxxxx"
The response echoes 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":
order_id,order_date,region,category,unit_price,quantity,amount
1001,2026-07-01,East,Electronics,1200,2,2400
1002,2026-07-01,North,Home,150,4,600
1003,2026-07-02,east,Electronics,1200,1,1200
1004,2026-07-02,South,Apparel,80,5,400
1005,2026-07-03,East,Home,150,3,450
1006,2026-07-03, East,Apparel,80,2,
1007,2026-07-04,North,Electronics,900,1,900
1008,2026-07-04,South,Home,150,2,300
1009,2026-07-05,East,Electronics,900,3,2700
1010,2026-07-05,east,Apparel,80,10,800
1011,2026-07-06,North,Apparel,80,3,240
1012,2026-07-06,South,Electronics,1200,1,1200
1013,2026-07-07,East,Home,150,6,900
1014,2026-07-07,East,Electronics,900,2,1800
1015,2026-07-08,North,Home,150,1,150
1016,2026-07-08,South,Apparel,80,4,320
1017,2026-07-09, East,Electronics,1200,2,2400
1018,2026-07-09,north,Home,150,5,750
1019,2026-07-10,East,Apparel,80,6,
1020,2026-07-10,South,Electronics,900,1,900
1014,2026-07-07,East,Electronics,900,2,1800
1021,2026-07-11,North,Electronics,1200,1,1200
1022,2026-07-11,South,Home,150,3,450
1023,2026-07-12,East,Home,150,2,300
1024,2026-07-12,North,Apparel,80,7,560
1025,2026-07-13,East,Electronics,1200,20,24000
The seeded problems:
#ProblemWhereCorrect handling
1Duplicate row1014 appears twiceDrop the copy, 26 rows → 25
2Missing amount1006, 1019Backfill with unit_price × quantity (160, 480)
3Inconsistent region spellingeast, north, East (with leading space)strip + case folding; 6 spellings merge into 3 regions
4Trap: extreme value1025 (1200 × 20 = 24000)Keep it. Amount is consistent with unit price × quantity — a real wholesale order
Problem 3 is downstream of 1 and 2: until region is normalized, 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 > North 4,400 > South 3,570;
  • by category: Electronics 38,700 > Home 3,900 > Apparel 2,960;
  • largest order: 1025 at 24,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:
curl -X POST "$AGENTSTUDIO_URL/files" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -F "file=@orders.txt"
Take the id:
export FILE_ID="file_xxxxxxxx"
You cannot mount immediately after upload. The response's status starts as checking; poll GET /files/{id} until it becomes available (other values: rejected / type_rejected):
curl -X GET "$AGENTSTUDIO_URL/files/$FILE_ID" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY"
# Watch the "status" field in the response body until it reads "available"
A 1.1 KB text file typically goes from 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_path must 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):
curl -X POST "$AGENTSTUDIO_URL/sessions" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent": "'"$AGENT_ID"'",
    "environment_id": "'"$ENV_ID"'",
    "title": "订单数据体检与分析",
    "resources": [
      { "type": "file", "file_id": "'"$FILE_ID"'", "mount_path": "/uploads/orders.csv" }
    ]
  }'
Take the id (shaped like sesn_<ULID>); the response also carries status (idle / running / terminated):
export SESSION_ID="sesn_xxxxxxxx"
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:
  1. 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.
  2. Exit when the session is idle and stop_reason.type == "end_turn". The session is idle whenever it waits for input (turn finished, or a self-hosted tool result needs backfilling) — only end_turn is a true exit signal.

Open the SSE stream first

The SSE endpoint requires the header Accept: text/event-stream:
# Keep this connection open in one terminal
curl -N -X GET "$AGENTSTUDIO_URL/sessions/$SESSION_ID/events/stream" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -H "Accept: text/event-stream"
After connecting you first receive a :connected comment line, then events arrive in groups of four lines (note: no space after the colon):
id:1
event:message
:HTTP_STATUS/200
data:{"object":"message","status":"completed","id":"sevt_...","type":"session_status","content":[...]}
The real payload is only the 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/...:
cat > msg.json <<'EOF'
{
  "input": [
    {
      "type": "message",
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "/mnt/session/uploads/orders.csv 是一份电商订单流水,列:order_id, order_date, region, category, unit_price, quantity, amount。\n\n先打印一下 pandas 的版本,确认环境里已经预装。然后把文件拷到 /mnt/user 下工作,完成两件事:\n\n1. 数据质量体检并清洗,把清洗后的数据写成 /mnt/session/outputs/orders_clean.csv。\n2. 基于清洗后的数据回答四个问题:总营收是多少;各 region 营收排名;各 category 营收排名;金额最大的单笔订单是哪一笔、占总营收多少。写成 /mnt/session/outputs/findings.md。\n\n体检发现的每个问题都要在 findings.md 里说明处理方式。两个产物都要用 mark_artifacts 登记。"
        }
      ]
    }
  ]
}
EOF

curl -X POST "$AGENTSTUDIO_URL/sessions/$SESSION_ID/events" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -H "Content-Type: application/json" \
  -d @msg.json
The task states what is wanted, not how to clean — discovering the problems and choosing the handling is the agent's job. The body's 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:
  1. The top-level status is not the session state. It is the status of the message itself (usually completed). The session state hides inside content[0].data.session_status.
  2. thread_id is top-level, not under metadata.
Event type values you will actually see:
typeMeaning
messageMessage text. role: user is the echo of what you sent, role: assistant is agent output; text in content[ ].text
reasoningThinking placeholder event — carries no content field (parse defensively; do not index d["content"] directly)
tool_callAgent invokes a tool; content[0].data = { name, arguments, call_id }
tool_call_outputTool result; content[0].data = { name, call_id, output }, where output is a JSON string
model_request_start / model_request_endStart/end of each model request; _end's content[0].data carries input_tokens / output_tokens / cache hits — good for cost accounting
session_statusSession state change, see below
The 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" } } → send tool_approval_response or function_call_output; do not exit
So the exit check is:
if d["type"] == "session_status":
    data = d["content"][0]["data"]
    if data["session_status"] == "terminated":
        break
    if data["session_status"] == "idle" and data.get("stop_reason", {}).get("type") == "end_turn":
        break

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):
  1. bash prints the version, confirming pandas is preinstalled;
  2. bash mkdir -p /mnt/user /mnt/session/outputs, then copies the csv from the read-only mount into /mnt/user;
  3. write a profiling script → bash runs it;
  4. Recovers from a problem on its own: the script gets named inspect.py, colliding with the standard library, so numpy's internal import inspect picks up the script and throws a traceback. The agent reads the error, mvs it to health_check.py, and re-runs cleanly;
  5. profiling output: (26, 7), 1 fully duplicated row, 2 missing amount values, 6 region spellings;
  6. write a clean+analyze script → bash runs it. The script includes a built-in check: rows where amount != unit_price × quantity count 0;
  7. write writes findings.md;
  8. Self-review: runs a bash python3 - <<EOF to re-read the output CSV and recompute;
  9. mark_artifacts registers both artifacts in one call, returning each file_id;
  10. assistant summary → end_turn.
Steps 4 and 8 are where the agent's self-directed iteration shows: nobody told it 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:
{
  "marked": [
    { "path": "/mnt/session/outputs/orders_clean.csv", "description": "...", "file_id": "file_0qwg..." },
    { "path": "/mnt/session/outputs/findings.md",      "description": "...", "file_id": "file_zckv..." }
  ],
  "failed": [ ]
}
If you did not note those IDs, list files by session scope (two query params, scope_type + scope_id — not the nested scope[type] form, which is silently ignored and returns the whole workspace's files):
curl -X GET "$AGENTSTUDIO_URL/files?scope_type=session&scope_id=$SESSION_ID&limit=100" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY"
After the turn ends, every file written to /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):
curl -X GET "$AGENTSTUDIO_URL/files/$CLEAN_FILE_ID/content" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" -o orders_clean.csv

curl -X GET "$AGENTSTUDIO_URL/files/$FINDINGS_FILE_ID/content" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" -o findings.md
Check them against the Step 3 answers first. This is the habit most worth building — the agent saying it got it right does not count; verify yourself:
python3 - <<'EOF'
import csv
from collections import defaultdict

rows = list(csv.DictReader(open('orders_clean.csv')))
by_region = defaultdict(float)
for r in rows:
    by_region[r['region']] += float(r['amount'])

assert len(rows) == 25, f"expected 25 rows, got {len(rows)}"
assert sorted({r['region'] for r in rows}) == ['East', 'North', 'South']
assert sum(float(r['amount']) for r in rows) == 45560.0
assert sorted(by_region.items(), key=lambda x: -x[1]) == [
    ('East', 37590.0), ('North', 4400.0), ('South', 3570.0)]
print("cleaned output matches expectations")
EOF
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").
The most interesting part is an extra section in the report — the agent did not delete the huge order 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:
curl -X GET "$AGENTSTUDIO_URL/sessions/$SESSION_ID" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY"
A typical run's 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:
curl -X POST "$AGENTSTUDIO_URL/sessions/$SESSION_ID/archive" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY"

curl -X POST "$AGENTSTUDIO_URL/environments/$ENV_ID/archive" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY"

curl -X POST "$AGENTSTUDIO_URL/agents/$AGENT_ID/archive" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY"
After archiving, the session's 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.pip are ready at container start; the agent spends no tokens or time on pip install.
  • Enable tools one by one: default_config.enabled: true is only the toolkit master switch; what the model actually receives is decided by enabled: true entries in configs[ ]. Skip that and the agent has only mark_artifacts.
  • Files are read-only mounts plus a writable workspace: uploads sit at /mnt/session/uploads/ (read-only); the agent must copy to /mnt/user or /tmp to modify, and writes artifacts to /mnt/session/outputs/. /mnt/user and /mnt/session/outputs may not exist in a fresh container — mkdir -p first.
  • 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 the file_id immediately with a description — no scan wait, no list digging, and one call registers multiple artifacts.
  • Drive with events, observe with SSE: a message event moves the agent into running; the SSE stream returns assistant text and tool calls in real time; use session_status + stop_reason to decide when to stop.
  • Disambiguate idle: the session state idle occurs both on "turn finished (end_turn)" and "tool result needed (requires_action)" — distinguish by stop_reason.type; only end_turn exits.
  • 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:
curl -X GET "$AGENTSTUDIO_URL/sessions/$SESSION_ID/events?order=desc&limit=1" \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY"
The returned 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:
{
  "data": [
    {
      "object": "message",
      "status": "completed",
      "id": "sevt_...",
      "type": "session_status",
      "content": [ { "type": "data", "data": { "session_status": "idle", "stop_reason": { "type": "end_turn" } } } ]
    }
  ],
  "next_page": "MTc4ODI0..."
}
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.
In production, when the agent runs for minutes and your handler cannot hold a connection, use polling. Production HITL ("wait for human approval") also does not depend on event push — combine "poll history + 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_path must start with /uploads/, read-only). mount_path decides the file name inside the sandbox.
  • Writable scratch area: /mnt/user or /tmp.
  • Retrievable artifacts: /mnt/session/outputs/ — written files are scanned and downloadable (no registration needed); mark_artifacts gets the file_id immediately with a description.
Easy-to-get-wrong API details:
ItemCorrect form
Update agentPOST /agents/{id} (not PATCH); body must carry current version and name
SSE streamGET /sessions/{id}/events/stream (without /stream it does not upgrade)
Message type in eventsmessage (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 locationcontent[0].data.session_status (top-level status is the message status, always completed)
Thread ID locationtop-level thread_id (not under metadata)
List files by session?scope_type=session&scope_id=sesn_... (nested scope[type] is ignored)
Download a fileGET /files/{id}/content (/download is 404)
Disambiguating idle: the session state idle occurs on both end_turn and "tool result needed" (requires_action); distinguish with stop_reason.type.

Next steps

Agent Context
Agent Governance & Evolution
Store
Application Marketplace