Organize a specialist sub-agent team with the multiagent coordinator pattern to produce a tailored sales proposal, observing every sub-agent precisely through the Session Threads API and thread-level events.
Overview
This page shows how to use the Managed Agents multiagent coordinator pattern to organize a team of specialist sub-agents that collaborate on a sales proposal, and how to observe and render each sub-agent precisely with the Session Threads API and the session-level event stream. Examples use curl (REST).
Scenario: the fictional company Northstar (selling a workflow automation platform to mid-market ops teams) wants automated sales proposals. Today, reps hand-craft each prospect's proposal — research what companies in that segment care about, pick two relevant case studies, price by the internal rules sheet, assemble a two-page document. Every step uses different data and judgment.
One coordinator agent runs three specialists: a researcher uses web search for segment priorities; a case-study picker reads the library for the two best matches; a pricing modeler reads only the rules file and seat count. Add a checker that sanity-checks alignment before the coordinator writes. The coordinator sequences them and writes the proposal.
Environment variables and models
Authorization: Bearer $DASHSCOPE_API_KEY. The multiagent configuration and session event streams are Managed Agents capabilities, currently cn-beijing only.
Use model IDs qwen3.8-max / qwen3.7-max / qwen3.7-plus. Every roster member is a full agent, and you can mix models per role — flagship models for high-value writing/coordination, cheaper tiers for mechanical file-reading and rule application, cost-split by model ID. This example uses qwen3.7-plus throughout for simplicity.
Two frequent issues in multi-agent scenarios, stated up front:
Define the three specialist sub-agents
Each sub-agent has its own system prompt, output shape, and only the tools it needs. The researcher gets only search; the picker only reads the local library; the pricing modeler sees only pricing_rules.md. Scoping tools by role prevents the modeler from pulling competitor prices off the web and keeps the whole case library out of the coordinator's context.
The orchestration tools are auto-injected by the platform in multiagent mode — no need to declare them in tools (the coordinator receives create_agent / list_agents / wait_for_agents; workers receive submit_result).
Record the
id returned by each POST /agents below (shaped like agent_<ULID>) — they go into the coordinator's roster.The researcher prospect_researcher (search MCP only)
mcp_servers.name is the marketplace service code (not existence-checked at declaration; a typo fails silently — troubleshooting in the production page). Marketplace services are called through the platform proxy; no credentials needed. To see a service's tool names: bl mcp tools --server WebSearch.
The case-study picker case_study_picker (local library only)
The pricing modeler pricing_modeler (rules file only)
Add a checker: pre-writing self-review
The three specialists each deliver a local conclusion; nobody is responsible for "do the three add up". So the roster gains a proposal_checker sub-agent: before the coordinator writes, it checks whether the picked cases and pricing framework align with the prospect's priorities.
Delegated once before writing, it reads the research, picks, and pricing framework, judges self-consistency and priority fit, and returns a verdict plus suggestions. It is a plain worker like the others: delegable, message-passing, running in its own thread and context.
The checker is pure judgment with no tools (an empty
tools array is fine — Advanced: Prompt Versioning and Rollback verified that a tool-less pure-LLM agent runs normally). For judgment-heavy work, set its model.id to a stronger model than the other workers — capability and cost tiering is done by choosing different model IDs; the model field has no effort-style dial.multiagent.agents[ ] member types: {"type":"agent","id":...} references a created sub-agent, optionally with version (see below); {"type":"self"} makes the coordinator itself delegable — at most one; a second raises AGENT_010 multiagent.agents 中 self 类型最多只能有一个.Give the team its materials and wire the coordinator
Materials
7 short case studies (healthcare/manufacturing/logistics/retail/finance/public sector) — enough to watch the picker choose the two that fit:
- St. Clair Health: regional hospital network, 6,200 staff, credentialing and prior-auth scattered across 11 systems → merged into 3 automated flows, prior-auth turnaround down 58%, $1.9M saved annually.
- BlueRidge Health Plan: regional payer, 2,800 staff, 19% of claims-exception emails needed rework → exception routing end-to-end automated, rework down to 6%, claims 11 days faster.
- Calder Manufacturing: industrial, 3,100 staff, PO approvals averaged 9 days → PO cycle down to 2.1 days, maverick spend down 14%.
- Northwind Logistics: 3PL, 4,400 staff, carrier onboarding 3 weeks each → down to 4 days, 22% more carriers activated in Q1.
- Harborview Retail Group: specialty retail, 5,600 staff, store inventory anomalies over Slack+spreadsheets → triage automated across 140 stores, stockouts down 31%.
- Aperture Payments: fintech, 1,900 staff, KYC and merchant onboarding averaged 6 business days → SLA down to 36 hours, 2.4× throughput with the same staff.
- Summit County Government: public sector, 3,700 staff, paper building permits across five departments → single digital intake with parallel review, median permit time 41 → 17 days.
- PRODUCT one-pager: Northstar is a workflow automation platform for mid-market ops teams — visual flow building, 200+ SaaS connectors, role-based approvals, SOC 2 Type II; typical results 40–60% fewer manual tickets, first workflow live in 3 weeks.
- PRICING rules: $65/seat/month, or $52 annual; usage tiers light 1.0x / standard 1.15x / heavy 1.30x multiplied onto the unit price; enterprise tier (>500 seats) adds $48,000/yr platform fee and drops the annual rate to $44/seat/month; every option includes onboarding, enterprise adds a dedicated CSM.
Upload the 9 files
Upload the 7 case-study markdown files, product_one_pager.md, and pricing_rules.md one by one. Upload via multipart, then poll GET /files/{id} to status=available before mounting.
file_id.
Create the Environment (sandbox)
The researcher's MCP search is proxied by the platform and does not traverse the sandbox network, but the picker and modeler read mounted files (sandbox tools), so the environment is still needed.
networking.type currently takes unrestricted (all egress allowed). The response id looks like env_<...>.Create the coordinator (with roster)
The coordinator references the four sub-agents (three specialists + checker):
id (agent_<ULID>) as $AGENT_ID.
Roster member version semantics: omitting
version neither snapshots at coordinator creation nor at session creation — leave the coordinator untouched, upgrade pricing_modeler from v1 to v2, start a session, and the delegated pricing thread runs v2 directly; upgrade to v3 after session creation but before messaging, and the thread still uses v3. In other words, editing a sub-agent does not require updating the coordinator. Conversely, for a "frozen, reproducible team", write version explicitly on the member; the platform accepts and echoes it (GET on the coordinator shows version: N on the member). This is dual to session version locking in Advanced: Prompt Versioning and Rollback: the session's agent field locks the coordinator's own snapshot, the roster's version locks the member reference; omit both and both take latest.Start the session (with the 9 file resources)
mount_path must start with /uploads/; the actual sandbox path is /mnt/session + mount_path. The 7 cases mount at /uploads/case_studies/<slug>.md, the other two at the /uploads/ root. Instruction texts use actual paths /mnt/session/uploads/....
The field is
agent (an object with just id locks the current latest snapshot; to lock an older one pass {"id": ..., "version": N}). The response's agent echoes the full configuration snapshot (system and tools, but not multiagent — the roster does not enter the session snapshot; member versions resolve at delegation per the table above). Also note: the resources[ ] file_id values in the response differ from what you submitted — the platform generates session-scoped file IDs at mount; the original upload IDs remain valid. The response id looks like sesn_<ULID>, saved as $SESSION_ID.Start the proposal and observe
Session creation takes no initial input, so the flow is create the session, then post one message event to start work.
mark_artifacts registers the artifact.
Streaming view (one session-level stream, split by thread_id)
SSE is a single session-level stream — the coordinator and all sub-agents converge into the same stream. There is no thread-level SSE endpoint: GET /sessions/{sid}/threads/{tid}/events/stream returns 404 {"type":"api_error","message":"not support"}. To stream, there is only the session stream; to watch a single thread, poll the Thread Events API below.
:connected, then event: message frames (with :keepalive interleaved). Every event carries a top-level thread_id — the split key: the coordinator's main thread is thrd_-prefixed (primary thread); each delegated sub-agent is an sthr_-prefixed session thread.
The four multi-agent-specific event types:
thread_created: a sub-thread appears. Thethread_idis the newsthr_...;content[0].datais{"agent_name": "prospect_researcher", "session_thread_id": "sthr_..."}.thread_message_sent: a message leaves a thread. The sender'sthread_id(thrd_for the coordinator,sthr_for workers),content[ ].textis the message body,metadata.to_session_thread_id/to_agent_namepoint at the recipient.thread_message_received: the same message from the recipient's perspective.thread_idis the receiving thread;metadata.from_session_thread_id/from_agent_nameidentify the sender (messages from the coordinator showfrom_agent_name: "coordinator").thread_status: thread state transitions.content[0].datais{"agent_name": ..., "thread_status": "running"|"idle", ...}, ending withstop_reason: {"type": "end_turn"}.
tool_call / tool_call_output / mcp_call / mcp_call_output / model_request_start|end / reasoning / message / session_status) are as in the getting-started page, just all carrying thread_id. The coordinator's auto-injected orchestration tools appear in the stream as follows:
The worker's return channel: the worker's final assistant text is automatically returned to the coordinator as
thread_message_sent; submit_result is a separate explicit submission tool — the two may each arrive in sequence. Sub-agent system prompts need no "return via send_to_parent" phrasing — no tool by that name exists; writing it may only make the model hesitate. Just say "return strict JSON".Without a long connection, poll the history instead:
GET /sessions/$SESSION_ID/events?order=asc. Note the 100-event page cap (a larger limit still returns 100); page with the response's next_page cursor: ...&page=<next_page>. A full multi-agent turn has 168 events — two pages.The Session Threads API: one row per sub-agent
Structured observation does not require the event stream — Session Threads offers four endpoints for thread-level list, detail, events, and archive (this API group is not yet in the official public docs):
List: GET /sessions/{session_id}/threads
parent_thread_id: the delegator's thread. Under the coordinator's main thread it isthrd_...; if a worker delegates its own sub-agents, that thread's parent is the worker'ssthr_...(multi-level delegation).agent.version: the member version this thread actually ran — the "omit version, take latest at delegation" rule from the table above is directly visible here. If the roster does not lock a version and the sub-agent upgraded to v2 mid-flight, new sessions show v2 here.status:running/idle/terminated. Polling this endpoint during a batch is a live sub-agent workbench.
limit / page match the other list endpoints; archived threads are excluded by default.
Get: GET /sessions/{session_id}/threads/{thread_id} — single-thread detail with the same fields (plus request_id). Two boundaries: the coordinator's main thread (thrd_-prefixed) is not in the threads collection — GET with its id returns 400 invalid_parameter "Invalid request"; the threads resource addresses only delegated sub-threads, the main thread being just a thread_id value on the event stream. Archived threads return the same 400 (absent from List too) — archiving removes a thread from the addressable space, but its events remain queryable (below).
Events: GET /sessions/{session_id}/threads/{thread_id}/events — the single thread's event sequence, a thread_id-sliced piece of the session stream: from thread_created (task received) through thread_status (idle + stop_reason), with its own model and tool calls in between. Without limit it returns the thread's full events; with limit the response carries a next_page cursor. This is the shortest path to "what did this one sub-agent do" — no filtering 168 session events by thread_id.
Archive: POST /sessions/{session_id}/threads/{thread_id}/archive
archived_at filled, updated_at refreshed); List no longer includes it; repeat archiving is idempotent. Use it to clear historical worker threads in long sessions, keeping the list to active ones — event data is unaffected.
What each specialist returned
Walking the four sub-threads' thread_message_sent (or the submit_result arguments.content) via Thread Events yields each specialist's raw JSON:
- researcher:
priorities/pain_points/sources— external intelligence (6bailian_web_searchcalls, visible asmcp_call/mcp_call_outputwithserver_label: "WebSearch"). - picker:
picks— glob the directory, read all cases, score, pick two (typical: St. Clair Health + Aperture Payments, healthcare-first, exactly right). - modeler:
options— reads the rules file and computes for 600 seats × heavy 1.30x (conservative tier $486,720 = 52 × 1.3 × 600 × 12, matching the rules file to the dollar). - checker:
aligned/issues/suggestions— one alignment verdict plus suggestions.
Client: render one stream into a team workbench
The client-side core is one data structure — a bucket dictionary keyed by thread_id — plus four behaviors that make rendering simpler than expected:
- Deduplicate by event
id. SSE offers no cursor or resume token; a reconnect may replay old frames — dedupe byid. - Assistant text arrives whole; no incremental assembly. Streaming text lands in one
completedframe, withsequence_numbertypicallynulland no multi-frame deltas; parse as "accumulate ifsequence_numberexists, otherwise treat as complete". The live feel comes from structural events:thread_created(who started),thread_status(who finished),mcp_call/tool_call(who is doing what). session_statusis the only session-level event. The turn's only 2 events withoutthread_idwere it (running, and idle+stop_reason) — treat it as session state, belonging to no thread.- The main thread has no
thread_created.thrd_exists before the stream starts — register the firstthrd_event you see as the coordinator.
GET /sessions/{id}/events as authoritative (order=asc + next_page paging, 100 per page, 168 events = 2 pages); when only a few threads matter, align per-thread with GET .../threads/{tid}/events instead.
Choosing the observation surface:
| Want | Use |
|---|---|
| Whole-team live progress (one connection) | Session-level SSE + the bucket dictionary |
| A "who runs / who finished" workbench | Poll GET /sessions/{id}/threads (status + agent.version) |
| Dig into one sub-agent | GET .../threads/{tid}/events (full single-thread) |
| Authoritative record / post-mortem | GET /sessions/{id}/events, page through everything |
Read the proposal — and a failure post-mortem
Retrieving the artifact: find the write tool_call for proposal.md in the coordinator's main-thread events (tool write, path /mnt/session/outputs/proposal.md, arguments.content is the full text — this run also registered it with mark_artifacts, so the file_id is immediately available), or find it by name in the Files list.
The correct scope filter for the Files list is
GET /files?scope_type=session&scope_id=<sesn_...> — passing session_id instead is silently ignored and returns the whole workspace's list (distinguish ownership per item by the scope field).- Run 1: all three workers correct, but the coordinator's proposal.md contained case names that do not exist in the library — "Community Health Network", "Bayshore Medical Group" — and a pricing table of $39,000/$58,500/$78,000 — the coordinator hallucinated while synthesizing the long context.
- Run 2: same configuration, same input; proposal.md contained the correct St. Clair Health + Aperture Payments.
- Correct workers ≠ correct team. After the architecture splits research/picking/pricing among specialists, the last mile of synthesis-writing remains a high-risk hallucination zone — the coordinator deserves the strongest model, and a prompt demanding "quote the workers' returned data verbatim; never rewrite numbers" helps.
- Attribution via the Threads API. When things go wrong,
GET /threads/{id}/eventsper thread: the picker's picks were right, the modeler's options were right, the error was in the coordinator's write arguments — the problem is pinpointed to the coordinator. A single agent leaves one undifferentiated log; multi-agent keeps an independent record per role, which makes attribution easier. - Evaluation belongs in the pipeline. The methodology of Advanced: Prompt Versioning and Rollback applies here too: before going live, run a fixed prospect profile and assert on proposal.md (case names must be in the library whitelist; pricing must match the rules sheet's computation) before releasing.
Why three specialists (+ one checker) instead of one agent
A single agent with all the tools could write the proposal too, but role-scoped tooling pays concretely:
- The modeler has only the rules file and no web path — it cannot pull competitor prices; pricing always derives from internal rules.
- The picker reads 7 files here, hundreds in production — that volume stays in the sub-agent's context, not the coordinator's.
- The coordinator decides only sequence and handoffs and does no specialist work, keeping its context clean.
proposal_checker extends the same idea: before writing, it pulls cases and pricing back onto the prospect's priorities, catching misalignment pre-write — here it drove 4 rewrites. The check runs in its own thread; the verdict and suggestions stay in the event log for review. The cost is one more worker thread — one more delegation round-trip and some runtime overhead. If a prospect scenario does not need the self-check, drop it from the roster; the coordinator proceeds straight to writing.
Wrap up (archive)
Use archive throughout (audit trail kept, metering stopped). Mind the order: a running session cannot be DELETEd (400 invalid_parameter) — wait for idle or archive; agents are archive-only:
Next steps
- Multi-Agent Collaboration: the coordinator roster field reference.
- Session Event Stream: event reference for single-agent scenarios.