Deploy an agent
This page covers everything about getting agent code onto the platform: what goes in your directory, how the upload works (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
There is no public API hostname yet — this platform is in alpha. Set CAI_API to the endpoint your administrator gives you, or leave it unset and platformctl opens a temporary connection to the platform for you. The raw HTTP examples below assume CAI_API and a token in CAI_TOKEN.
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 the platform's registry. The image tag is a content hash of your source, and the running service is pinned to the image digest (a fingerprint like
localhost:30500/my-agent@sha256:...). Pinning by digest means the exact bytes you built are the exact bytes that run — a tag can be moved, a digest cannot. - Rolls out a new revision (an immutable snapshot of image + settings) and routes traffic to it once it is healthy.
The agent's status 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:
| Framework | Required file | It must define | Optional |
|---|---|---|---|
| ADK | agent.py | a module-level root_agent | requirements.txt, other .py files |
| LangGraph | graph.py | a module-level graph (a compiled graph) | requirements.txt, other .py files |
| CrewAI | crew.py | a module-level crew | requirements.txt, other .py files |
The CLI auto-detects the framework from the entry file: crew.py means CrewAI, graph.py means LangGraph, anything else defaults to ADK. You can force it with --framework adk|langgraph|crewai.
Each framework guide has a complete example: ADK, LangGraph, CrewAI.
Deploy with the CLI
- Deploy the directory.
--namedefaults to the directory's name; names must be a lowercase DNS label (letters, digits, hyphens; max 63 characters) and are permanent for the agent's life:
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
status: -> building
status: building -> deploying
status: deploying -> ready
my-agent is ready at http://my-agent.cai-p-x7k2q.svc.cluster.local
The CLI polls status every 2 seconds for up to 5 minutes. On failure it prints my-agent failed to build/deploy (see 'platformctl logs my-agent') — but for build failures, check platformctl status my-agent first: the build output is in the message field (see below).
- Confirm it:
platformctl status my-agent
Deploy over raw HTTP
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. |
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. |
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. All errors use the standard envelope {"error": "<message>", "request_id": "<id>"}.
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 unbounded. You can change all of this later with PATCH /v1/agents/{name}/config — every change rolls a new revision. See traffic and revisions.
Poll build status
Poll GET /v1/agents/{name} until status is ready or failed:
curl -s -H "Authorization: Bearer $CAI_TOKEN" "$CAI_API/v1/agents/my-agent"
You should see:
{
"name": "my-agent",
"status": "ready",
"kind": "agent",
"framework": "adk",
"image": "localhost:30500/my-agent@sha256:9f2c1a...",
"url": "http://my-agent.cai-p-x7k2q.svc.cluster.local",
"public_url": "https://my-agent-x7k2q.apps.codyhill.dev",
"latest_revision": "my-agent-00001",
"message": "",
"owner": "you@example.com"
}
urlis the agent's internal cluster address, and out of the box it is the only address that serves anything. New agents are private by default: the platform marks them cluster-local, so the authenticated control plane is the only way to invoke them. See invoke your agent for how to publish one deliberately.public_urlis the address the agent would have if you published it (https://<name>-<project-short>.apps.codyhill.dev). It is returned even while the agent is private, so treat it as a reservation, not a live endpoint.external_urlappears only once that address genuinely answers over a valid certificate.messagecarries the last error, and this 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
From your machine: run platformctl deploy again with the same name. Same build flow, new revision, same URL.
From stored source: every deploy also stores your source files (best-effort) so you can edit them in the browser — see files and the editor. Saving a file does not change the running agent; the save response says so: saved - redeploy the agent for this to take effect. To rebuild from what is stored, no tarball needed:
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.
From the console: the deploy dialog at https://console.codyhill.dev offers three input modes — write code in the browser (with starter templates per framework), upload files or a folder, or upload a ready .tar.gz. The browser refuses binary files and names the offending path; use the .tar.gz mode for those.
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; a stale build claim is reclaimable 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" | 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 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 more that is not an HTTP error: a deploy can build fine and then hang at deploying. If the project hit its service quota after the build started, the status message 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 the 409 above.
For runtime failures after a successful deploy (crash loops, missing root_agent, model auth), see troubleshooting.
Clean up
platformctl delete my-agent
You should see:
deleted my-agent
This removes the running service, the agent's secrets and settings, and its record.
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.