Deploy an agent
Deploying is how your code becomes a running agent. This page covers what goes in your directory, how the upload works from both the CLI and raw HTTP, how to watch a build, how redeploys work, and what every deploy error actually means.
Before you begin
- You have an account. Accounts are created by an administrator or through an invitation link — ask your administrator if you don't have one. See create an account.
- You have the CLI installed. See install the CLI.
- You are signed in. Deploying is a management action and always requires credentials (invoking an agent, by contrast, does not by default):
platformctl login --email you@example.com
- Your project is connected to Crusoe Cloud. Your image is built into a repository in your own Crusoe Cloud Registry, so a project with no credential cannot deploy — the first deploy is refused before anything is built. A project admin connects it once, and nothing on this page changes for a project that already is. See connect your Crusoe Cloud account, or Crusoe Cloud integration for the full reference.
The raw HTTP examples below read the API address from CAI_API and a token from CAI_TOKEN. Set CAI_API=https://api.codyhill.dev. platformctl defaults to that address and needs neither variable set.
What a deploy does
When you deploy, the platform:
- Packages your directory into a
.tar.gzarchive (the CLI and the console do this for you). - Builds a container image from it: your code is layered onto the framework's base image, and your
requirements.txtis installed. - Pushes the image to a repository in your own Crusoe Cloud Registry — the storage images are pulled from when a container starts. The repository is created for you on the first build of each workload and named
cai-<project-short>-<workload>, so you never pre-create one. The image's tag, its readable label, is a hash of your source: a short fingerprint computed from the file contents. The running service is pinned to the image digest instead, a fingerprint of the built image itself, written likeregistry.us-east1-a.ccr.crusoecloudcompute.com/cai-ab12cd-my-agent@sha256:.... Pinning by digest means the exact bytes you built are the exact bytes that run. Someone can move a tag to point at different bytes; nobody can move a digest. - Rolls out a new revision — an immutable snapshot of the image plus its settings, frozen the moment it is created — and routes traffic to it once it is healthy.
The agent's state walks building → deploying → ready, or ends at failed. Deploying again with the same name is a redeploy: same flow, new revision, and the agent keeps its URL.
Directory layout per framework
Your directory needs one entry file, named for the framework, plus an optional requirements.txt. The CLI auto-detects which framework you meant from that file name: crew.py means CrewAI, graph.py means LangGraph, anything else defaults to ADK. Force it with --framework adk|langgraph|crewai.
- ADK
- CrewAI
- LangGraph
my-agent/
agent.py # required: defines a module-level `root_agent`
requirements.txt # optional
from google.adk.agents import Agent
from crusoe_adk.foundry import foundry_model
from crusoe_adk.tools import run_python, search_memory
root_agent = Agent(
name="my_agent",
model=foundry_model(),
instruction="Use run_python for math and search_memory to recall facts.",
tools=[run_python, search_memory],
)
Full contract and the built-in tools: build agents with ADK.
my-agent/
crew.py # required: defines a module-level `crew`
requirements.txt # optional
from crewai import Agent, Crew, Process, Task
import crusoe_crewai as crusoe
research_buddy = Agent(
role="Research Buddy",
goal="Compute things and recall what you were told to remember.",
backstory="A concise assistant running on the Crusoe AI Platform.",
llm=crusoe.foundry_model(),
tools=[crusoe.RunPython(), crusoe.SearchMemory()],
verbose=False,
)
respond = Task(
description=(
"Prior conversation (may be empty on the first turn):\n{history}\n\n"
"Now respond to the user's current message:\n{message}"
),
expected_output="A helpful, concise answer to the user's current message.",
agent=research_buddy,
)
crew = Crew(agents=[research_buddy], tasks=[respond], process=Process.sequential)
crew may also be a zero-argument function returning a Crew. A task description must reference {message} — that is how the user's input reaches the crew.
Full contract, including the tool rule that catches everyone once: build agents with CrewAI.
my-agent/
graph.py # required: defines a module-level compiled `graph`
requirements.txt # optional
from langgraph.prebuilt import create_react_agent
import crusoe_langchain as crusoe
graph = create_react_agent(
crusoe.foundry_model(),
tools=[crusoe.run_python, crusoe.search_memory],
prompt="Use run_python for math and search_memory to recall facts.",
)
graph must be compiled — a StateGraph you called .compile() on — and its state must be LangGraph's MessagesState. create_react_agent returns exactly that.
Full contract: build agents with LangGraph.
You can add other .py files and import them from the entry file. If you include a requirements.txt, its packages are installed at build time; a package that cannot be installed fails the build, not the running agent, and the reason is readable in the agent's message field.
Deploy
A name must be a lowercase DNS label — the kind of name allowed in a web address, so lowercase letters, digits, and hyphens only, up to 63 characters. It is permanent for the life of the agent.
- platformctl
- curl
- Console
--name defaults to the directory's name:
platformctl deploy ./my-agent --name my-agent
You should see:
packaging ./my-agent...
uploading my-agent (1.2 KiB, framework=adk)...
build 2f6f2f6e-8a1e-4c3b-9d2a-1b2c3d4e5f6a accepted
state: -> building
state: building -> deploying
state: deploying -> ready
my-agent is ready at https://my-agent-x7k2q.apps.codyhill.dev
The CLI checks the state every 2 seconds for up to 5 minutes, and packages the tarball for you. On failure it prints my-agent failed to build/deploy (see 'platformctl logs my-agent'). For a build failure, though, run platformctl status my-agent first — that is where the build output lands, in the message field described below.
Confirm it:
platformctl status my-agent
The API endpoint is POST /v1/agents. It accepts multipart/form-data (the standard file-upload encoding) with these fields:
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
name | text | yes | — | Lowercase DNS label, max 63 characters. Immutable. |
code | file | yes | — | A .tar.gz of your agent directory. |
framework | text | no | adk | One of adk, langgraph, crewai, function, container. |
runtime | text | no | python | Functions only (python, nodejs, go, ruby). Sending it with an agent framework is a 400. |
config | text | no | — | A JSON string with the same shape as PATCH /v1/agents/{name}/config, applied to the first revision. |
Any other field name is a 400 naming the field. Package and upload:
tar -czf my-agent.tar.gz -C my-agent .
curl -s -X POST "$CAI_API/v1/agents" \
-H "Authorization: Bearer $CAI_TOKEN" \
-F "name=my-agent" \
-F "framework=adk" \
-F "code=@my-agent.tar.gz"
You should see (HTTP 202 — the build runs in the background):
{"agent": "my-agent", "build_id": "2f6f2f6e-8a1e-4c3b-9d2a-1b2c3d4e5f6a"}
The upload is capped at 100 MiB by default. Every error comes back in the platform's standard JSON wrapper, {"error": "<message>", "request_id": "<id>"}. The request_id identifies your one request in the platform's records — quote it when you ask for help.
To deploy into a specific project, add ?project=<slug> to the URL. It is a query parameter, never a form field.
The config field
Use config to set scaling, resources, or the request timeout on the very first revision, instead of patching afterwards. Omitted fields keep platform defaults:
curl -s -X POST "$CAI_API/v1/agents" \
-H "Authorization: Bearer $CAI_TOKEN" \
-F "name=my-agent" \
-F "code=@my-agent.tar.gz" \
-F 'config={"scaling":{"min_scale":0,"max_scale":5,"container_concurrency":10},"timeout_seconds":300}'
The shape also accepts resources with requests and limits for CPU and memory, for example {"requests":{"cpu":"100m","memory":"256Mi"}}. Setting max_scale to 0 means no ceiling on how many copies of your agent may run at once. You can change all of this later with PATCH /v1/agents/{name}/config. Every such change rolls a new revision. See traffic and revisions.
At https://console.codyhill.dev, go to Compute → Agents → Deploy agent. The dialog offers three ways to provide code:
- Write it in the browser, starting from a template for your framework.
- Upload files or a whole folder. The browser refuses binary files — images, compiled libraries, anything that is not text — and names the file it rejected.
- Upload a
.tar.gzyou already have. Use this mode for anything binary.
Click through Review & deploy. The build panel streams building, deploying, ready. If it lands on failed, the build output appears on the agent's Overview tab — that is the build log.
Scaling, concurrency, container resources, and the request timeout are on the agent's Configure dialog after the first deploy.
Poll the build state
The state walks building → deploying → ready, or ends at failed. A plain ready boolean travels beside it, true exactly when the state is ready.
- platformctl
- curl
- Console
platformctl deploy already polls for you and exits when the state settles. To check afterwards:
platformctl status my-agent
The table shows name, framework, state, ready, address, and image — and, on a failure, the message field described below. Add -o json for the raw response.
The address column shows the agent's public URL, or private when the agent has not been published — never the internal hostname, which would look like an address you could call but is not. The one field the CLI does not decode is latest_revision; for that, run platformctl agents revisions my-agent or read the curl tab's response.
Poll GET /v1/agents/{name} until state is ready or failed:
curl -s -H "Authorization: Bearer $CAI_TOKEN" "$CAI_API/v1/agents/my-agent"
The build panel on the agent's page streams the state as it changes, and the agent's Overview tab shows where it landed. On a failure, the build output appears in the failure panel there.
The full response is:
{
"name": "my-agent",
"state": "ready",
"ready": true,
"kind": "agent",
"framework": "adk",
"image": "registry.us-east1-a.ccr.crusoecloudcompute.com/cai-ab12cd-my-agent@sha256:9f2c1a...",
"url": "http://<private-hostname>",
"public_url": "https://my-agent-x7k2q.apps.codyhill.dev",
"latest_revision": "my-agent-00001",
"owner": "you@example.com"
}
stateis the agent's own vocabulary —building,deploying,ready,failed— and is the field to show a person.readyis the boolean beside it, true only while the state isready, and is the field to branch on in a script. Every resource on the platform publishes the same pair, so one habit works everywhere.urlis the agent's internal address, reachable only from inside the platform. Out of the box it is the only address that serves anything. New agents are private by default. The one way in is the control plane — the platform's own management API, the thingplatformctland the console talk to — and it requires you to sign in. See invoke your agent for how to publish an agent deliberately.public_urlis the address the agent would have if you published it:https://<name>-<project-short>.apps.codyhill.dev, where<project-short>is your project short id. The platform returns it even while the agent is private, so treat it as a reservation rather than a live endpoint. A separate field,external_url, appears only once that address genuinely answers over a valid TLS certificate.messagecarries the last error. It is omitted entirely when there is nothing to say, which is why the healthy response above has nomessageat all. When it is there, it matters more than it looks:
There is no separate build-log endpoint. When a build fails, the tail of the actual build output — the real pip error, the real syntax error — lands in the agent's message field, truncated at 4,000 characters with the suffix ...(truncated). Read it with platformctl status my-agent or in the console's failure panel before anything else.
Redeploy
Deploying again with the same name is a redeploy: same flow, new revision, same URL.
Every deploy also keeps a copy of your source files so you can read and edit them later — see files and the editor. The copy is best-effort, meaning the platform tries but does not guarantee it. Saving a file does not change the running agent, and the save response says so: saved - redeploy the agent for this to take effect.
- platformctl
- curl
- Console
Run the same command again:
platformctl deploy ./my-agent --name my-agent
This uploads your local directory, so it is the right choice whenever your machine holds the truth.
Upload a fresh tarball to POST /v1/agents, exactly as for the first deploy. Or rebuild from what the platform already stored, with no tarball at all:
curl -s -X POST -H "Authorization: Bearer $CAI_TOKEN" \
"$CAI_API/v1/agents/my-agent/redeploy"
You should see:
{"agent": "my-agent", "build_id": "8c41...", "files": 2, "note": "rebuilding from the stored source"}
If nothing is stored yet, you get 400: no source is stored for this agent - deploy it once from the CLI or upload files first.
The deploy dialog offers the same three ways to provide code as the first deploy: write it in the browser, upload files or a folder, or upload a .tar.gz.
The editor opens every stored file as its own tab precisely so this is safe. The two upload modes are not: they define the complete tree, so a file you leave out is deleted, and the platform keeps no history to restore it from.
Limits
| Limit | Value |
|---|---|
| Code upload | 100 MiB (platform default) |
| Stored source (editor) | 1 MiB per file, 200 files per agent, 64 MiB expanded archive |
| Agent name | lowercase DNS label, max 63 characters, immutable |
| Concurrent builds | one per agent; if a build crashes, its claim on the agent is released after 30 minutes |
Full platform limits: limits reference.
Deploy errors, verbatim
These are the real error strings the API returns, so you can search for them.
| Status | Error | What it means and what to do |
|---|---|---|
400 | missing or invalid 'name' (must be a lowercase DNS label) | Fix the name: lowercase letters, digits, hyphens; must start with a letter; max 63 characters. |
400 | unsupported 'framework': one of "adk", "langgraph", "crewai", "function", "container" | Typo in the framework field. |
400 | missing 'code' file field: ... | The code part was absent or not a file field. |
400 | 'runtime' only applies to framework=function | Drop the runtime field for agents. |
400 | invalid 'config' field: ... | The config value is not valid JSON of the expected shape. |
409 | this project cannot deploy yet: its container images are built into your own Crusoe Cloud container registry, and no Crusoe Cloud credential is mapped to this project. A project admin sets one with PUT /v1/projects/{id}/crusoe-cloud ... then deploy again. The repository itself is created for you on the first deploy - there is nothing to pre-create. Nothing was built and your stored source was not touched. | The project has no Crusoe Cloud connection. A project admin connects it — platformctl crusoe-cloud connect, or Project Settings in the console — then you deploy again. See connect your Crusoe Cloud account. |
409 | this deploy could not be attached to a project, and images are built into the project's own Crusoe Cloud container registry - so there is no registry to push to. Deploy with a project-scoped credential, or name the project explicitly with ?project=<slug>, then deploy again. Nothing was built. | The request resolved to no project, so there is no credential to look up. Sign in with a credential that belongs to a project, or add ?project=<slug> to the request. |
409 | this project is at its service limit (N / M): deploying needs at least one more service and cannot proceed. Delete an agent or function, or ask an admin to raise the project's service quota, then deploy. | Your project has a quota on how many services it can run. Delete something (platformctl delete <agent>) or ask a project admin to raise the quota. |
409 | a build is already in progress for agent my-agent | Someone (or a script) is already deploying this agent. Wait for it, or wait 30 minutes for a stale claim to expire. |
403 | an agent named my-agent already exists and has no recorded owner, so only a project admin can redeploy it | The agent was deployed without a signed-in owner. A project admin can adopt it by redeploying it. |
404 | unknown agent: my-agent | Either the agent doesn't exist, or it belongs to someone else — the API deliberately doesn't tell you which. |
One failure is not an HTTP error at all: a deploy can build fine and then hang at deploying. That happens when the project hits its service quota after the build already started. The agent's message field explains it: This project is at its service limit (N / M), so the new revision cannot get a network route yet - .... The fix is the same as for the 409 above.
For runtime failures after a successful deploy (crash loops, missing root_agent, model auth), see troubleshooting.
Clean up
- platformctl
- curl
- Console
platformctl delete my-agent
You should see:
deleted my-agent
curl -s -X DELETE -H "Authorization: Bearer $CAI_TOKEN" \
"$CAI_API/v1/agents/my-agent"
You should see:
{"agent": "my-agent", "deleted": true}
On the agent's page, click Delete and confirm.
What delete removes, and what it leaves behind
Delete is narrower than most people expect. It tears down the agent's runtime and configuration, and it does not touch the data the agent produced.
| Removed by delete | Left behind |
|---|---|
| The running service and its public address | The agent's conversations (sessions), stored in MemoryStore under keys like sess:<project-short>:my-agent:<session-id> |
| The per-agent Secret | |
| The environment, compute and memory-policy settings | |
The agent's long-term memory and knowledge — the VectorDB collections mem_<project-short>_my-agent and know_<project-short>_my-agent | |
| The agent's record, and with it the source files you stored, its build records, and its saved log history |
If you have been asked to remove a customer's data, or you are handing a project over, deleting the agent is not enough. Its sessions - the transcripts - survive, and there is no single command that removes them all.
What the agent remembered does go with it: deleting the agent deletes its memory and knowledge collections. To erase one person's memories while the agent stays, use platformctl agents memory forget <agent> --user <id> --yes (see Long-term memory).
Sessions are kept indefinitely unless your administrator configured an expiry.
Removing an agent's data before you delete it
Do this first, while the agent is still running. The session routes read through the agent itself, so they stop working the moment it is gone.
Sessions are indexed per end user. There is no "every session" view, by design — it is the same boundary that keeps one end user's history out of another's. So the walk is always: list the users, then list each user's sessions, then delete them one at a time. There is no bulk-delete route.
- platformctl
- curl
- Console
platformctl agents users list my-agent
platformctl agents sessions list my-agent --user 7c9e6679-7425-40de-944b-e07fc1f90ae7
platformctl agents sessions delete my-agent 3f2c8a1e-9d41-4c1b-a2f7-0b1c2d3e4f5a
The owning user is resolved from the session itself, so sessions delete does not need --user.
curl -s -H "Authorization: Bearer $CAI_TOKEN" \
"$CAI_API/v1/agents/my-agent/users"
user_id is required on the session list — without it the call fails with 400 the user_id query parameter is required:
curl -s -H "Authorization: Bearer $CAI_TOKEN" \
"$CAI_API/v1/agents/my-agent/sessions?user_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
Then delete each one:
curl -s -o /dev/null -w '%{http_code}\n' -X DELETE \
-H "Authorization: Bearer $CAI_TOKEN" \
"$CAI_API/v1/agents/my-agent/sessions/3f2c8a1e-9d41-4c1b-a2f7-0b1c2d3e4f5a"
You should see:
204
A session id that is already gone returns 404.
The agent's Overview tab has a Users & Sessions section listing the end users, their conversations, and each transcript.
Reading is all it offers here. To actually remove a conversation, use the platformctl or curl tab above — this is the step that removes customer data, so it is worth not assuming.
A transcript can carry whatever the end user typed and whatever the agent's tools were called with. Treat it as customer data. See sessions for the full list-and-read surface.
- Memory bank collection cleanup. Memory bank collections (
mem_<project-short>_<agent>) are platform-managed collections. To remove a memory bank collection and its stored entries, request your platform administrator to remove themem_<project-short>_<agent>collection directly from the underlying vector store.
Only after that is done should you delete the agent.
Next steps
- Invoke your agent — talk to what you just deployed.
- Framework guides with complete examples: ADK, LangGraph, CrewAI.
- Secrets and environment variables — configure the agent without redeploying code.
- Traffic and revisions — roll back by moving traffic between revisions.
- Deploy from CI with a service account — automate this on every merge.