Run modes
Coming soonWrite your agent and harness once, then choose where it runs.
- in-process while you build in development
- promote to a durable worker you host yourself in production, and register against Workflows API, so that Mistral handles orchestration and durability for you
- or run it as a durable managed worker in production : Mistral hosts both the orchestration layer and the agent - you provide your agent definition as a python package.
| Local | self-hosted Worker | managed Worker | |
|---|---|---|---|
| API | agent.run(), agent.create_session() | agents serve | agents deploy |
| Infrastructure | none (in-process) | Mistral-managed workflow + a worker process you run | Mistral-managed workflow and worker |
| Multi-turn in one session | ✓ | ✓ | ✓ |
| Callbacks, steer, interrupt | ✓ | ✓ | ✓ |
| Durable and distributed | ✗ | ✓ | ✓ |
| Session resume after a crash | ✗ | ✓ | ✓ |
| Fork, manual compaction, cancel | ✗ | ✓ | ✓ |
| Best for | dev, tests, fast iteration | production on infrastructure you control | production with nothing to operate |
All three run modes are driven from the SDK with agent.create_session(), and the session API is the same in each one. Only the rows above change.
Local - in-process
The default. agent.run(...) runs in your process with no infrastructure. Best for development, tests, and fast iteration.
agent = agents.Agent(model="mistral-medium-3-5", instructions="Be concise.")
result = await agent.run("Say hello in three words.")agent.run(...) is a single question with no memory; a session is a conversation.
agent.run(...) is a single question with no memory. agent.run() has no callback channel. With permission_mode="ask" the turn stops at the approval and does not terminate. Use create_session().
A session does not need a worker. create_session() opens one in this process, and it remembers the
turns you run through it:
async with await agent.create_session() as session:
await ask(session, "My name is Ada.")
answer = await ask(session, "What is my name?") # the session remembersask() is the small event-reading helper from Sessions.
An in-process session also answers callbacks, and steer_turn and interrupt_turn both work on a
running turn. What it does not have is durability: the session lives and dies with your process, and
fork_session, compact_session and cancel_session raise UnsupportedSessionProcedureError.
What the CLI loads
agents info, agents serve and agents deploy all need the same thing: the Agent
object you built in Python. By default they look for agent in main.py, so a project that is a
single main.py runs as it is.
# main.py
from mistralai.agents import agents
agent = agents.Agent(
model="mistral-medium-3-5",
name="hello-agent",
instructions="You are a friendly agent. Greet the user in one sentence.",
harness=agents.Harness(tools=[get_weather]),
)get_weather is an @agents.tool function defined in the same module — see Tools.
To keep the agent somewhere else, declare it in pyproject.toml as module:attr — the module that
defines it, then the variable that holds it:
[tool.mistralai.agents]
entrypoint = "src.service:support_agent"The module is imported with the project root on the import path, so a module sitting at the root
works without the project being an installable package. Every command takes a project root as its
argument, so agents serve ./hello-agent runs one you are not standing in.
The agent must be named
Agent(...) has to set name= explicitly. It is the name the worker registers and the name
agents deploy routes on, so the SDK refuses to guess it:
main:agent must set an explicit name: `agents.Agent(model=..., name=...)`. The name is the deployed workflow name.This name is what the control plane keys on, and it is unique per workspace, not globally:
deploying hello-agent again from the same workspace updates that same deployment, but another
workspace's hello-agent is a separate one.
The platform keeps some names for its own agents: vibe-code, vibe-code-web and
vibe-code-agent, written with - or _. Agent(...) raises SdkError for a reserved name,
and agents serve and agents deploy refuse one passed with --name:
Agent name 'vibe-code' is reserved by the platform. Choose another name with `agents.Agent(model=..., name=...)` or `--name`.Check what was loaded
agents info prints the surface the CLI resolved, and names the first thing it could not load:
agent: hello-agent
model: mistral-medium-3-5
description: ...
instructions: You are a friendly agent. Greet the user in one sentence.
sandbox: none
tools:
- get_weather
connectors: (none)
skills: (none)
hooks: (none)
betas: (none)
subagents: (none)Run it after changing the harness. A tool that does not appear here will not appear to the model either.
:::note [tool.mistralai.agents].runtime is rejected
Older projects pinned a backend with runtime = "remote" or runtime = "nuage". There is one
runtime now, so the key selects nothing and the CLI errors on it rather than ignoring it. Delete the
line if a project still carries it.
:::
agents serveSelf-hosted worker - agents serve
You run the worker; Mistral runs the orchestration. State survives crashes and scales across workers - with the same agent and harness code.
agents serve is a way to test what a deployed agent would do, or to host and operate this worker in production in your own cluster.
agents serve loads the agent you built in Python:
export MISTRAL_API_KEY="<your-key>"
agents serve # the project in the current directory
agents serve ./hello-agent --name hello-agentIt runs a long-lived worker registered against the Workflows API and prints a link to trigger the agent.
--name overrides the agent's defined name, which is also what the run is routed on.
✓ Agent hello-agent is serving on agents-sdk-hello-agent
Try the agent:
• easily in Vibe's UI: https://chat.mistral.ai/code?agent=hello-agent
• or programmatically, using the Session API: https://docs.mistral.ai/studio/agents/core/sessions
Debug: https://console.mistral.ai/build/workflows?workflowSearch=hello-agent
Press Ctrl+C to stop.Registration is what points the agent name at this deployment. Until it succeeds no session can
reach the worker, so serve treats it as fatal: the banner appears only once the agent is
registered, and otherwise it exits non-zero naming the control plane it tried.
Cannot serve agent: registration failed at https://api.mistral.ai/v2/agents: <detail>
The worker cannot receive sessions until it is registered.A managed worker skips this step: agents deploy registers the
agent when it creates the deployment.
The usual causes are a control-plane URL that does not end in /v2/agents, an agents API that is not
enabled for the account, and a name already bound to another workflow or deployment - most often one
you created earlier with agents deploy. Agent names are unique per workspace, so serve under a free
name (--name hello-agent-dev) or shut down the agent holding it — see
Agent lifecycle.
The worker is where your agent actually runs. Stop it and the agent stops answering, but ongoing sessions are safe : durability and workflow state is managed by Workflows API. So the session will resume from its last checkpoint when a worker comes back.
Note that the harness executing actions from anyone opening session against this agent - whether it's from Vibe UI or from the Session API - runs on the machine hosting the worker. Be careful to provide isolation through a Sandbox.
:::note Worker environment
Against production a worker needs only MISTRAL_API_KEY (workflows-enabled). To point one at another
deployment, staging here, move both URLs together:
export MISTRAL_API_KEY="<staging key>"
export MISTRAL_API_URL="https://api.globalaegis.net"
export AGENTS_SDK_DEPLOY_CONTROL_PLANE_URL="${MISTRAL_API_URL}/v2/agents"MISTRAL_API_URL selects the Workflows and model APIs. It is the API root, without /v1.
SERVER_URL and MISTRAL_SERVER_URL still work as deprecated aliases.
AGENTS_SDK_DEPLOY_CONTROL_PLANE_URL is where the worker registers itself. It has to end in
/v2/agents: the SDK appends its own /agents segment, and that doubled path is the real route.
Pointing it at the bare host is a 404 and serve will refuse to start.
:::
:::note Model rate limit
Agents running in parallel share the workspace's model rate limit, and a burst of completions can
hit 429. Set AGENT__LLM_RATE_LIMIT on the worker to cap model calls across every worker of the
deployment:
export AGENT__LLM_RATE_LIMIT='{\"time_window_in_sec\": 60, \"max_execution\": 30}'Completions over the budget wait in the queue instead of failing. The cap counts calls, not tokens, and it is off when unset. See Workflows rate limiting for how it is enforced. :::
:::tip When serve fails to start
agents serve prints only warnings and the banner. Set AGENTS_SDK_DEBUG=1 for the full worker
log, which names the Workflows API it resolved and the Temporal endpoint it connected to. LOG_LEVEL
does not work here.
Fail to connect to Temporal Service Client is the error for any failed connection, including one
the server refused. An expired or revoked MISTRAL_API_KEY reports as a connection failure rather
than an authentication one, so check the key before looking at the network.
:::
agents deployManaged worker - agents deploy
Mistral runs the worker too. Nothing to host, nothing to keep alive. A deploy builds your project's Dockerfile, so add one at the project root and commit it:
# Dockerfile
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
ENV UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=never
WORKDIR /app
# Pinned: a deploy rebuilds this image, so an unpinned version changes the runtime under you.
RUN --mount=type=cache,target=/root/.cache/uv \
uv pip install --system --prerelease=allow mistralai-agents==1.1.0rc4
COPY . /app
CMD ["agents", "serve"]COPY . /app takes everything in the build context, so add a .dockerignore beside the Dockerfile
before your first deploy:
# .dockerignore
.env
.venv/
.git/
__pycache__/
*.py[oc]
build/
dist/The first line is the most critical one. An image layer keeps whatever it copied, so a .env
that lands in the image is readable by anyone who can pull it.
The project is built from your git checkout, so commit and push first, and then deploy:
agents deploy --name hello-agent--dockerfile points at another path. If neither that nor ./Dockerfile exists, the deploy stops
before creating anything.
--name overrides the name= on your Agent and is what the deployment is keyed on. Without it,
the agent keeps the name= from your code, the same name that agents serve uses.
An agent that acts on behalf of the user who starts a session — it uses all_connectors(), its named
connectors all keep the default run_as="auto", or it uses a Mistral
sandbox behind the sandbox proxy — is deployed
hardened by default, the same way agents serve hardens on its own. An unhardened deployment could be
taken over by anyone, and its worker could not register anyway. --no-hardened opts out; --hardened
forces it on for agents without such needs.
If the worker fails to start, agents deploy exits with the rollout error and prints the worker's
last output, such as a traceback or a missing dependency:
Deploy failed. Rollout failed: Service is unhealthy.
Last output from the worker:
Traceback (most recent call last):
...
ModuleNotFoundError: No module named 'my_tools'To read the full logs, pass the managed deployment id that agents deploy prints after
Created agent:
curl -s "https://api.mistral.ai/v1/runtime/logs/<managed-deployment-id>?log_type=runtime&order=asc&limit=1000" \
-H "Authorization: Bearer $MISTRAL_API_KEY"The container command
The container must run only the worker, because agents deploy already registered the agent.
agents run-worker does this. It needs a mistralai-agents release newer than 1.1.0rc6:
CMD ["agents", "run-worker"]The platform sets DEPLOYMENT_NAME, and run-worker polls that exact task queue.
To start the worker from your own Python code instead, for example in a Helm chart, call
run_worker() on the agent:
import asyncio
from main import agent
asyncio.run(agent.run_worker())Outside a managed deployment, set DEPLOYMENT_NAME to the task queue to poll. If you do not set it,
the worker reads the queue of the agent with the same name from the control plane, so register the
agent first with agents serve or agents deploy.
If your platform routes sessions to other names, pass them as
run_worker(workflow_name=..., deployment_name=...).
An image with CMD ["agents", "serve"] keeps working. serve detects the managed deployment,
skips the registration, and prints a warning that asks you to change the CMD.
Secrets
A managed worker can get a secret from the Secret API as an environment variable, bound at deploy time.
Create the secret first. --secret-binding only points at one reference; it cannot create it. The
secret has to already exist in the deployment's workspace. Create it in the Secrets Manager, at
console.mistral.ai → Secrets, and note the name you give
it — that name is what the reference below points at.
Then bind it:
agents deploy . --name hello-agent \
--secret-binding COMPANY_LLM_API_KEY=secret:workspace:company-llm-key \
--secret-binding ANOTHER_API_KEY=secret:workspace:another-api-keyThe option takes a reference, not a value. secret:workspace:<NAME> names the secret you
created above; the platform reads it at boot, inside the pod. No secret material is typed on the
command line, sent to the control plane, or stored with the deployment. A value passed here
instead of a reference is rejected before the deploy starts.
Each deploy must state the complete set. A later deploy that omits --secret-binding unbinds every
secret an earlier one bound, so keep the options in whatever script or CI job runs the deploy.
The variable is set for the running worker and for the image build, so a Dockerfile that needs
one at build time can declare a matching ARG.
:::warning Build args are not a secure channel A value passed to the build persists in the image's history and layers, and anyone who can pull the image can recover it. Only bind credentials you are willing to rotate, and prefer reading the variable at run time, where it is resolved in the pod and never written to a layer. :::
Studio tracing
A worker sends traces of its agent turns, model calls, and tool calls to Studio, including prompt and completion content. This is on by default for agents serve and agents deploy, and needs no extra package or setting. Content is redacted on the worker before it's sent, with a default policy that masks common secrets and personal data.
Viewing traces in Studio requires an Enterprise organization with Observability enabled. See Observability access.
Set AGENTS_SDK_STUDIO_TRACING_INCLUDE_CONTENT=0 to drop content, or AGENTS_SDK_STUDIO_TRACING_ENABLED=0 to turn tracing off. On a managed worker, set them with ENV in the Dockerfile.
See Send traces for the settings, and Explore traces for how traces map to sessions and turns.
To change what is redacted, see Data redaction.
What durability buys you
On a worker, the runtime makes the session durable: each turn, tool call, and sandbox operation is checkpointed. If the worker process dies, the run resumes from the last checkpoint rather than starting over.
Human-in-the-loop. Because the session is durable, it can pause for human input - an approval, an answer - and resume when that input arrives, even days later, without keeping a process running. See Permission.
Choosing
- Reach for local while you build, test, and iterate - zero setup, instant feedback.
- Promote to a self-hosted worker when you need durability, scale-out, or human-in-the-loop and the agent has to run on infrastructure you control - inside your network, next to your data, or under your own deployment process.
- Reach for a managed worker for everything else in production. It is the same durability without a process to operate, at the cost of deploying from a git repository.
No code rewrite in either direction: same Agent, same Harness, wrapped in a workflow.
Time, sleeping, ids and randomness
On a worker, a workflow is replayed: when a worker picks the run back up, the agent loop
re-executes from the top and Temporal feeds it the recorded outcomes. Anything that reads the
outside world during that re-execution answers differently the second time round, which breaks
the run in one of two ways. If the value only lands in data, nothing raises and the behaviour is
quietly wrong - a deadline computed from time.time() moves forward at every replay, so a
"stop after 10 minutes" limit never arrives. If it changes which activity runs next, Temporal
fails the workflow task and retries it forever, and the run stops making progress.
The SDK gives you four replay-safe replacements. Each one works from all three places agent code runs - the agent loop, a hook, and a tool body - so a shared helper needs no branching:
from datetime import timedelta
from mistralai.agents import agents
deadline = agents.now() + timedelta(minutes=10) # stable across replay
job_id = agents.uuid4() # same id on replay of this run
await agents.sleep(30 + agents.random().uniform(0, 5)) # jittered poll| Instead of | Use | Why |
|---|---|---|
time.time(), datetime.now() | agents.now() | Workflow time in the agent loop, so a deadline holds across replay. Returns an aware UTC datetime; call .timestamp() for epoch seconds. |
asyncio.sleep(...) | await agents.sleep(...) | A durable server-side timer in the agent loop. In a tool body it is a plain bounded sleep, and raises if it would outlast the activity's start_to_close_timeout. |
uuid.uuid4() | agents.uuid4() | Re-deriving the id when the run is replayed yields the same value. A continue-as-new starts a new seed, so carry ids that must outlive it in your own state. |
random.random() | agents.random() | Returns a seeded random.Random, so .random(), .uniform(), .choice() and .shuffle() all replay identically. |
See also
- Sessions - driving a worker-backed session across turns.
- The Harness - capabilities that travel local-to-worker unchanged.
- Agent lifecycle - what a deploy creates, and shutting an agent down.