Take Managed Agents to production — MCP toolkit integration, configuration injection (session environment variables and the vault), Deployment scheduling, human-in-the-loop review, and resource lifecycle management, all with curl.
Overview
This page covers the production hardening of Managed Agents: MCP toolkit integration (the marketplace registry model), configuration injection (session environment variables + the vault), platform-managed triggers (Deployment: manual / cron / initial_events), human review (HITL), resource lifecycle management, and the current data-residency behavior. Examples use curl throughout.
The deep dive on credential injection (vault placeholder security model, egress gateway, allowed_hosts) lives in Advanced: Credential Vault Security and the Egress Gateway — this page only wires credentials up by the shortest path.
Scenario: Yunling Tea's morning briefing robot
"Yunling Tea" is a fictional chain tea-drink brand. The first thing its ops team does every morning is skim industry news: competitor launches, supply-chain ripples, any negative press involving tea drinks. It is repetitive, time-consuming, easy to miss — a textbook automation target. The morning-briefing robot strings together all the production infrastructure:
| Need | Capability |
|---|---|
| Run automatically at 7:30 every morning, unattended | Deployment (cron trigger) |
| Carry the day's task description when triggered | Deployment (initial_events delivered at trigger) |
| Search industry news | MCP toolkit (marketplace WebSearch service) |
| Push the briefing to the company GitHub repo | Vault (inject GITHUB_TOKEN; placeholder + gateway replacement) |
| Pause for human confirmation on major negative press | HITL (requires_action + polling) |
| Seasonal prompt changes, quarterly shutdown, cleanup | Versioning + resource lifecycle |
Why the vault instead of session environment variables on the scheduled path? Because Deployment does not support
environment_variables (silently ignored when passed; the triggered session has none) — vault_ids is the only credential path for unattended pipelines. For application-created sessions, environment variables come first (see Step 4).Prerequisites
- A Bailian workspace and its workspace ID;
- one DashScope API Key (one key covers all resources in the workspace);
- one GitHub PAT (the dummy value
GITHUB_TOKENbelow — this page only verifies the injection, and a fake token is safer).
Managed Agents is currently single-region (
cn-beijing). All requests carry Authorization: Bearer $DASHSCOPE_API_KEY; responses carry x-request-id for troubleshooting (the body also echoes request_id).The model: two extension lines + one trigger line
Two mutually independent lines matter most in production for extending an agent:
- MCP toolkits: the agent declares one or more marketplace services; at runtime the platform calls those services' tools on the agent's behalf. Fits "service on the public internet, hosted by the Bailian marketplace" — no connection details to manage, and usually no key either.
- Configuration / credential injection (two routes, layered by sensitivity):
- Session environment variables (
environment_variables, first choice): key-value pairs passed at session creation, cleartext straight through to the sandbox process; the agent reads real values viaos.environin bash / python. Fits non-sensitive config and interactive sessions — simplest, nothing to pre-create. Limitation: Deployment does not support them. - The vault: credentials are stored in the vault beforehand; sessions only carry
vault_ids. Inside the sandbox you get a placeholder (BMA_SECRET_PLACEHOLDER_*); the real value is substituted into theAuthorizationheader only when the egress gateway matches the credential'sallowed_hostswhitelist. Fits high-sensitivity secrets and unattended pipelines. Full security model in Advanced: Credential Vault Security and the Egress Gateway.
- Session environment variables (
function_call event; you compute and backfill with function_call_output. Use this when the service is only reachable on your internal network.
This page adds the trigger line: in the getting-started usage, your application creates the session and sends the messages — "who sends the task at 7:30 every morning" can only be a homegrown cron script. Deployment hands that role to the platform: declaratively configure "which agent, locked to which version, which messages delivered at trigger, which credentials attached, what schedule" — from then on the platform does the triggering, session creation, and initial_events delivery; your application only reads the results.
Security boundary: once a token is stored in the vault it never enters the sandbox —
printenv shows only a placeholder; at egress the gateway substitutes per the allowed_hosts whitelist. The placeholder guarantee covers environment variables and outbound requests (before substitution); the response body of the called service (e.g. an echo endpoint returning Authorization verbatim) enters the event stream as tool output — that is the external service's own behavior and outside the placeholder guarantee. Two more boundaries: the gateway only replaces the Authorization header; sandbox egress HTTPS passes through a gateway MITM, but the sandbox preinstalls the gateway CA (SSL_CERT_FILE / CURL_CA_BUNDLE / REQUESTS_CA_BUNDLE all point to it) — curl and requests pass default validation; investigate CA configuration only when a specific environment hits certificate errors. Details and troubleshooting in Advanced: Credential Vault Security and the Egress Gateway.Step 1 · Create the vault
A vault has two key fields: display_name (for the console) and metadata (your own internal user ID etc.):
id shaped like vlt_01M1G0W54ARXJ4W61B0N4281TK:
Step 2 · Store the GitHub token credential
Credentials live under the vault at the nested endpoint /vaults/{vault_id}/credentials (there is no top-level /credentials; it 404s).
Each credential = variable name + value + egress whitelist. auth.type currently supports only environment_variable (static_bearer / mcp_oauth return CREDENTIAL_AUTH_TYPE_ERROR). allowed_hosts is required and must sit inside auth.networking (omitting it → 409 CREDENTIAL_AUTH_NETWORKING_ERROR "egress network address must not be empty"):
secret_name: the variable name. Inside the sandbox,os.environ["GITHUB_TOKEN"]yields the placeholderBMA_SECRET_PLACEHOLDER_GITHUB_TOKEN— the real value is substituted into theAuthorizationheader only for egress requests hitting whitelisted hosts.secret_value: the real secret. Write-only — a GET returns onlysecret_name/type/networkinginauth, never the value.networking.allowed_hosts: the domain whitelist the secret may travel to, with wildcards (*.github.com). The token is only substituted into requests to these hosts — requests to other domains keep the placeholder, preventing exfiltration.- The response
idis prefixedvcrd_(notcred_).
Step 3 · Create the agent with an MCP toolkit
Three things to prepare separately: declare the MCP server and toolkit in the agent definition, the credential in the vault (Step 2), and wire with vault_ids at session / deployment creation (Steps 4 / 5).
Create the agent. mcp_servers declares the marketplace service with type: "official" and name set to the marketplace service code; enable tools via mcp_toolkit:
configs must enable tools one by one. mcp_toolkit mirrors builtin_toolkit: default_config.enabled: true alone is not enough — every tool you want (here bailian_web_search) must be explicitly enabled: true in configs[ ], otherwise the agent does not have the tool at runtime, with no error raised.https://{workspace_id}.cn-beijing.maas.aliyuncs.com/api/v1/mcps/{code}/sse — you generally provide no credentials; for services needing your own key, store it in the vault and call directly from bash with an Authorization header (bash sees the placeholder; the gateway substitutes the real value).
builtin_toolkit (bash/read/write, etc.) as covered in the getting-started page — the two coexist fine. A pure-MCP agent can even skip the environment — a session with an empty environment_id runs too.
Step 4 · Manual pilot: verify tools and the credential path
Before scheduling, run one plain turn verifying two things: the tool call really happens, and the credential path really works.
First choice: session environment variables (non-sensitive config / interactive sessions)
For application-created sessions, the first choice for config injection is environment_variables — cleartext key-value pairs straight into the sandbox, no vault to pre-create, real values directly available via os.environ:
printenv BRIEF_MAX_ITEMS → outputs 5 (the real value). The creation response and GET echo the field — only put non-sensitive content there.
Interactive session + non-sensitive config → environment variables are enough. Need secret-grade security (no cleartext in the sandbox) or the Deployment unattended path → wire the vault below.
Required on the scheduled path: vault_ids wiring
Deployment does not support environment_variables, so scheduled-path credentials (Step 5) can only use the vault. Attach credentials at session creation with vault_ids (the agent field takes the agent ID string; the session locks that agent's current version snapshot):
sessions.create accepts no initial input field; the standard pattern is two steps — create the session, then POST one message event. (For "trigger with the first message attached", use Deployment below, which supports initial_events.)printenv GITHUB_TOKEN verbatim:
event: line is always message, comment lines include :connected / :HTTP_STATUS/200 / :keepalive):
Authorization header, the gateway swaps in the real PAT, and GitHub authenticates.
Send another message to trigger the MCP tool ("search the latest tea-drink industry news with the tool"); MCP calls produce their own event types, parallel to tool_call:
tool_call / tool_call_output; MCP tools as mcp_call / mcp_call_output. The turn ends when session_status = idle and stop_reason.type == end_turn.
Pilot passed — both foundations work. Next, hand "who sends the task at 7:30" to the platform.
Step 5 · Deployment: scheduling on the platform
A Deployment is a resource at the same level as agent / environment / session: declare "which agent version to lock, what to deliver at trigger, which credentials to attach, what schedule" — the platform then does the triggering, session creation, and initial_events delivery. Console operations are covered in Deployments.
Create: version-locked agent + initial_events + vault_ids
agentmust be an object{id, version}; an ID string is rejected (400 PARAMS_ILLEGAL). This differs fromsessions.create(a string = lock latest). A Deployment is a long-lived resource and locks the agent version explicitly at creation — the response echoes the full snapshot of that version (system / tools / model). Version semantics in "Version semantics: locking and the drift trap" below.initial_eventsis the input array delivered at trigger time, shaped exactly like theinputused to message a session. It fills the gap left bysessions.createnot accepting initial input: in the generated session, the first user message is it — "trigger and run", with no follow-up message needed from your application.vault_idsis wired at the deployment level: every session generated by a trigger automatically carries these credentials (injection behavior is identical to manual session creation).
schedule gives the manual mode (trigger only via POST /run); providing it enables scheduling (see the cron section below). A successful creation (200) echoes all fields with status: "active" and an id like depl_xxx (prefix depl_).
Trigger: run and deployment_run
deployment_run (prefix drun_); session_id is null until async generation completes:
session_id (also how production decides "is it done" — status becomes succeeded, finished_at set):
{data: [...], next_page, request_id}, newest first. trigger_source distinguishes "manual" from "schedule" (cron).
Trigger and run: one full trace
Pull events for the generated session (GET /sessions/{id}/events?order=asc, or SSE); a typical trigger yields about 26 events, key frames:
initial_events enters the session as the first user message, the MCP tool is invoked, and vault credentials inject into the deployment-generated session (placeholder in the sandbox, gateway replacement at egress). Your application sent no messages at all — triggering and execution are fully handled by the platform.
Version semantics: locking and the drift trap
A deployment locks {id, version} at creation; later agent upgrades do not affect it. Upgrade morning-briefing's prompt from v1 to v2 (POST /agents/{id}; name + current version required, see Advanced: Prompt Versioning and Rollback), then trigger the same deployment:
POST /deployments/{id} (PATCH semantics; omitted fields are preserved). But beware a production trap:
For contrast: a session locks "latest-or-specified at creation" as a snapshot; a deployment locks "explicitly specified at creation", but updates without agent drift to latest.
cron: scheduled triggering
Adding schedule switches the deployment to scheduled mode. To verify scheduling use an every-minute expression (switch to your business timetable for real use):
* * * * *, every minute):
- Auto-triggers precisely on the minute; a new run with
trigger_source: "schedule"appears in the runs list, structurally identical to a manual run (same session creation, sameinitial_events). - The
scheduleobject in the response carriesnext_run_at/last_run_at— to know "is the schedule alive, when is the next run", query the deployment object instead of computing cron yourself. - The real briefing scenario:
"expression": "30 7 * * *"(07:30 daily),"timezone": "Asia/Shanghai".
Ops verbs: pause / unpause / archive
| Verb | Endpoint | Behavior |
|---|---|---|
| Pause schedule | POST /deployments/{id}/pause | status: "paused", paused_reason: {type: "manual"}; cron stops firing, next_run_at freezes |
| Resume | POST /deployments/{id}/unpause | status: "active"; resumes from the next schedule point; missed triggers during pause are not replayed |
| Archive | POST /deployments/{id}/archive | archived_at set, next_run_at cleared (schedule removed); GET still works, lists filter archived by default |
| Delete | DELETE /deployments/{id} | 405 not supported — like agents, archive-only |
- pause blocks only the schedule, not manual triggers:
POST /runagainst a paused deployment still triggers and runs normally. Use pause when you need to stop the schedule but keep manual triggering. - archive is the full stop:
POST /runagainst an archived deployment returns409 DEPLOYMENT_STATE_CONFLICT "deployment archived"— manual triggers fail too. - Concurrent triggers are not limited: issuing
POST /runagain while a previous run is still in progress creates another run, and both run to completion. Control concurrency on the trigger side (or adjust the schedule in an update) if you need limits.
Observability summary
| Question | Where to look |
|---|---|
| Is the schedule alive; when does it next run | GET /deployments/{id} → status + schedule.next_run_at |
| Did a given run finish | GET /deployments/{id}/runs → status / finished_at |
| What did a run do | the run's session_id → GET /sessions/{id}/events (or SSE) |
| Did it fail | the run's error field + the session's session_status events |
session.status_idled and let the platform POST your receiver.
Step 6 · HITL in production
Production human review should not depend on long connections. An approval window can span minutes to hours; holding an SSE connection across it neither scales (connections bind to instances) nor survives restarts (one deploy drops all in-flight approvals). The right shape: let the session park in a waiting state; your service fetches status, decides, and backfills when needed.
Managed Agents sessions natively support this "stop and wait" semantics — when the agent needs human confirmation for a tool call, the session turns idle with requires_action and stays there until you backfill the decision.
Option A: poll history + requires_action
Your service periodically pulls the latest history event and checks whether the session is parked in "needs human/tool intervention": the last event shows session_status: idle with stop_reason: requires_action, carrying pending_batch_id and the pending pending_call_ids.
tool_approval_response event (batch_id from stop_reason.pending_batch_id, call_id from the matching entry in pending_call_ids):
result to deny. When a batch has multiple pending calls, backfill them one by one (one call_id each); tools execute only after the whole batch is collected. To backfill a self-hosted function result, use function_call_output (with call_id + output).
In the briefing scenario: before the agent writes a "negative-press alert" into the company repo, have it call an action requiring confirmation; the session parks at requires_action; your service notices via polling, pushes to the ops on-call group; a human clicks allow/deny; you backfill tool_approval_response; the agent continues. Tool approval configuration is in Built-in Tools.
On cost caps: sessions currently provide no budget field and no over-limit notification event. Control cost at the Deployment layer via trigger frequency and concurrency limits, plus after-the-fact usage reconciliation.
Option B: Deployment trigger + query afterwards
If the trigger is "scheduled" or "one task fired by an external system", use the Step 5 Deployment directly: the external system just calls POST /deployments/{id}/run (or leaves it to cron), then collects results via GET /deployments/{id}/runs — status reaching succeeded only means execution finished; also verify the artifact content programmatically (e.g. check each briefing item's publish date falls inside the time window and the source URL parses) instead of treating run status as content acceptance. session_id leads to the artifacts. No long connection anywhere.
Step 7 · Data residency and region
Current behavior: Managed Agents is single-region (cn-beijing); agent inference and sandbox execution both happen in that region. There is no inference-region parameter and no per-session region override at sessions.create.
This section therefore needs no configuration: residency is guaranteed by the single-region fact. If your compliance requirement is "data stays in a region", the current shape satisfies it; multi-region selection will follow official releases.
Step 8 · Resource lifecycle: list / retrieve / update / archive / delete
Each resource has a set of verbs, but not every resource supports the full set:
| Resource | List/get | Update | Archive | Delete |
|---|---|---|---|---|
| agents | GET /agents, /agents/{id} | POST /agents/{id} (auto version bump) | POST /agents/{id}/archive | not supported (DELETE 405) |
| environments | GET /environments, /{id} | POST /environments/{id} | POST /{id}/archive | DELETE supported |
| sessions | GET /sessions, /{id} | — | POST /{id}/archive | DELETE supported |
| deployments | GET /deployments, /{id} (lists filter archived) | POST /deployments/{id} (mind agent drift) | POST /{id}/archive | not supported (405) |
| vaults | GET /vaults, /{id} | POST /vaults/{id} | POST /{id}/archive | DELETE supported |
| credentials | GET /vaults/{id}/credentials | — | — | DELETE /vaults/{vid}/credentials/{cid} |
POST /agents/{id}, not PATCH (405); the body must carry the current version for optimistic concurrency; it must carry name (otherwise AGENT_005 agent name must not be empty). A successful update bumps the version by 1:
archive keeps records for audit, tears down the container, stops metering; delete removes entirely. Prefer archive in production. Agents and deployments are archive-only; version history and run records remain queryable after archiving.
Step 9 · Cleanup
Cleanup has a dependency order, plus one trap: deleting a vault does not cascade to its credentials. Delete the vault first and the credential endpoint still works (orphan data). The correct order is child-before-parent:
session_id values from GET /deployments/{id}/runs and DELETE each (note: a running session cannot be deleted — wait for idle).
Summary
This page assembled the production-operations skeleton:
- MCP toolkits (
mcp_serversofficial +mcp_toolkitconfigs enabling tools one by one) let agents call marketplace services through the platform proxy, visible asmcp_call/mcp_call_outputin the stream. - Config injection has two layers: non-sensitive config via session environment variables (cleartext into the sandbox, first choice for interactive sessions); high-sensitivity secrets via the vault (placeholder injection + gateway replacement per
allowed_hosts; the token never touches your application, the agent config, or the sandbox) — the scheduled (Deployment) path supports only the latter. - Deployment absorbs triggering into the platform:
agent {id, version}locks explicitly,initial_eventsdelivers the first message at trigger, cron fires automatically,pause(blocks schedule, not manual) /unpause(missed runs not replayed) /archive(full stop) manage the lifecycle,GET runscollects results. - Production HITL two ways: poll
requires_action+ backfilltool_approval_response, or Deployment trigger + query runs afterwards. - Lifecycle verbs differ by resource: agents and deployments are archive-only; sessions / environments / vaults are deletable; delete credentials before vaults (no cascade).
{id, version}), no concurrency rejection on triggers (rate-limit yourself), no session budget field (throttle at the deployment layer), single region cn-beijing, mcp_servers.name is not existence-checked (typos fail silently), sandbox egress goes through gateway MITM (gateway CA preinstalled, default TLS validation passes; investigate CA configuration only for environment-specific errors).
Next steps
- Advanced: Credential Vault Security and the Egress Gateway: the placeholder model, gateway behavior, and troubleshooting.
- Advanced: Webhook Event Notifications: let the platform notify you when a task finishes.
- Advanced: Prompt Versioning and Rollback: canary and rollback for the briefing prompt.