Quickstart: deploy your first agent
In about five minutes you'll deploy a real AI agent, talk to it, and clean it up. The agent can run Python code in an isolated sandbox, and it remembers what you told it earlier in the same conversation.
Every step below is shown three ways — platformctl, curl, and the console. Pick whichever tab suits you and stay in it; the tabs are the same work through three different doors. Your choice follows you across every page in these docs.
If anything goes wrong, jump to If something breaks at the bottom — the common failures and their exact error messages are listed there.
Before you begin
-
A platform account and a selected project — see Create an account.
-
platformctltab: the CLI built and signed in — see Install the CLI. -
curltab: a token and the API address in your shell:export CAI_API=https://api.codyhill.devexport CAI_TOKEN="<your session token or API key>" -
Console tab: nothing else. A browser is enough.
-
A Crusoe Cloud access key (an access key ID and its secret key) for step 1, and the admin role in your project so you can save it. Create the key in Crusoe Cloud — the platform never mints one for you. If someone has already connected your project, you need neither.
-
A model (inference) API key for your project, because the agent's deploy is refused without one. There is no platform-wide model key: every project brings its own Crusoe Managed Inference key, saved once under Project Settings in the console (or
PUT /v1/projects/{id}/inference). If your project already has one — check on that page — you need nothing here.
Step 1: Connect your Crusoe Cloud account
Your agent is built into a container image — a self-contained bundle of your code and everything it needs to run — and that image is stored in a repository in your own Crusoe Cloud Registry. Your account, your quota, your bill, your retention policy, and no other customer's images sitting next to yours. That is why the platform needs a Crusoe Cloud credential for your project before it can build anything, and why a deploy without one is refused up front rather than failing halfway through a build.
This is a one-time step per project. If your project is already connected, nothing here changes for you — skip to step 2.
- platformctl
- curl
- Console
Check first — if this says you are connected, go to step 2:
platformctl crusoe-cloud show
An unconnected project answers:
not connected: this project has no Crusoe Cloud credential.
connect one with 'platformctl crusoe-cloud connect --access-key-id <id>'.
So connect it. Put the secret key in a shell variable (or a file), because it is read from standard input and never from a flag:
read -rs CRUSOE_SECRET_KEY # paste the secret key; it is not echoed
printf %s "$CRUSOE_SECRET_KEY" | platformctl crusoe-cloud connect --access-key-id 'CRUSOEEXAMPLEKEYID'
You should see:
connected - the credential was accepted by Crusoe Cloud and stored.
crusoe cloud project: ml-platform
project id: b6f1a0c2-1f2e-4a55-9a4e-2c0a7f8d3e11
region: us-east1-a
There is deliberately no --secret-key flag: a secret passed as an argument lands in your shell history and in the process table. Pipe it in, as above, or point at a file with --secret-key-file ./secret-key. Read it back with --secret-key-file - if you prefer to be explicit about standard input.
Two things that occasionally bite: if your key can reach more than one Crusoe Cloud project the command is refused until you add --cc-project-id <id>, and the command needs to know which platform project it is acting on — pass --project, set $CAI_PROJECT, or run platformctl config set-project.
These routes name the project in the path, so you need its id as well as your token. platformctl projects list prints it in the ID column, and the console shows it on the project's page:
export CAI_PROJECT=<your project id>
Check first — {"mapped": false} means you still need to connect:
curl -s "$CAI_API/v1/projects/$CAI_PROJECT/crusoe-cloud" \
-H "Authorization: Bearer $CAI_TOKEN"
Then save the credential. Building the body with jq keeps the secret key out of your shell history and out of the process table:
export CRUSOE_ACCESS_KEY_ID=CRUSOEEXAMPLEKEYID
read -rs CRUSOE_SECRET_KEY # paste the secret key; it is not echoed
jq -n --arg id "$CRUSOE_ACCESS_KEY_ID" --arg key "$CRUSOE_SECRET_KEY" \
'{access_key_id: $id, secret_key: $key}' \
| curl -s -X PUT "$CAI_API/v1/projects/$CAI_PROJECT/crusoe-cloud" \
-H "Authorization: Bearer $CAI_TOKEN" \
-H 'content-type: application/json' --data @-
You should see:
{"mapped": true,
"cc_project": {"id": "b6f1a0c2-1f2e-4a55-9a4e-2c0a7f8d3e11", "name": "ml-platform"},
"region": "us-east1-a"}
If your key can reach more than one Crusoe Cloud project the call is refused with 400 — this credential can access N Crusoe Cloud projects; set cc_project_id to choose one — so add cc_project_id and send it again. The region key is present only when a region is known.
- Sign in at https://console.codyhill.dev, press Cmd+K / Ctrl+K to pick your project, then go to Project Settings.
- If the page already names a Crusoe Cloud project, you are connected — go to step 2. Otherwise click Map to Crusoe Cloud.
- Paste the Access key ID and Secret key. Leave Crusoe Cloud project blank; that field appears only if your key turns out to reach more than one, and then it offers you a picker.
- Click Map project.
You should see: the dialog closes, a confirmation naming the Crusoe Cloud project, and the page listing your buckets, repositories, and models.
You do not create a repository yourself. The platform creates one per workload the first time it builds it, so there is nothing to pre-create here.
The secret key is checked against Crusoe Cloud before it is stored, so a typo fails here rather than as a mystery three commands later — and once stored it is never returned by any read. Full detail, including what happens when you disconnect, is in Crusoe Cloud integration.
Step 2: Write the agent
An agent is a directory with one entry file in it. Which file, and what it must define, is the only thing that differs between frameworks — the platform runs all three unmodified.
- ADK
- CrewAI
- LangGraph
ADK is Google's open-source Agent Development Kit. Your directory needs an agent.py defining root_agent.
mkdir my-first-agent && cat > my-first-agent/agent.py <<'EOF'
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_first_agent",
model=foundry_model(),
instruction="Use run_python for math and search_memory to recall facts.",
tools=[run_python, search_memory],
)
EOF
root_agentis the name the platform looks for. It must be defined at module level — the top level of the file, not inside a function.foundry_model()returns the platform-managed model. Swap models later via configuration, not code.run_pythonandsearch_memorycome fromcrusoe_adk.tools.
CrewAI describes agents by role, gives them tasks, and groups them into a crew. Your directory needs a crew.py defining crew.
mkdir my-first-agent && cat > my-first-agent/crew.py <<'EOF'
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,
verbose=False,
)
EOF
crewis the name the platform looks for — acrewai.Crew, or a zero-argument function returning one.- A task description must reference
{message}. That placeholder is how the user's message reaches your crew. Reference{history}too and the platform replays earlier turns into it. crusoe.RunPython()andcrusoe.SearchMemory()are classes, so note the parentheses.
LangGraph models an agent as a graph: nodes do work, edges decide what happens next. Your directory needs a graph.py defining graph.
mkdir my-first-agent && cat > my-first-agent/graph.py <<'EOF'
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.",
)
EOF
graphis the name the platform looks for, and it must be a compiled graph over LangGraph'sMessagesState.create_react_agentreturns exactly that.- The
MessagesStateshape is what lets the platform replay the conversation into your graph each turn — that replay is why turn 2 remembers turn 1. crusoe.run_pythonandcrusoe.search_memoryare plain tool objects, no parentheses.
You don't need a requirements.txt. Each framework's base image — the prebuilt starting point your agent is built on top of — already ships the framework, the Crusoe helpers, and these tools.
Two of those helpers are worth naming now, because every agent uses them:
run_pythonexecutes Python in a single-use sandbox — the agent's calculator.search_memorysearches the agent's long-term memory bank. It's empty today; that's fine.
Step 3: Deploy it
- platformctl
- curl
- Console
platformctl deploy ./my-first-agent --name my-first-agent
You should see:
packaging ./my-first-agent...
uploading my-first-agent (1.2 KiB, framework=adk)...
build 2f6f1c3a-... accepted
state: -> building
state: building -> deploying
state: deploying -> ready
my-first-agent is ready at https://my-first-agent-ab12cd.apps.codyhill.dev
The framework= on the upload line is whichever framework you wrote in step 2 — the CLI works it out from the entry file in your folder: crew.py means CrewAI, graph.py means LangGraph, anything else means ADK. Force it with --framework adk|crewai|langgraph.
The CLI polls every 2 seconds and waits up to 5 minutes.
Package the directory yourself and post it as multipart/form-data:
tar -czf my-first-agent.tar.gz -C my-first-agent .
curl -s -X POST "$CAI_API/v1/agents" \
-H "Authorization: Bearer $CAI_TOKEN" \
-F "name=my-first-agent" \
-F "framework=adk" \
-F "code=@my-first-agent.tar.gz"
Use framework=crewai or framework=langgraph if that is what you wrote. You should see HTTP 202 — the build runs in the background:
{"agent": "my-first-agent", "build_id": "2f6f1c3a-8a1e-4c3b-9d2a-1b2c3d4e5f6a"}
Poll until state settles on ready or failed. Every resource on the platform answers with the same triple: state is the word to show a human, ready is the boolean to branch on, and message says why when it is not ready:
curl -s -H "Authorization: Bearer $CAI_TOKEN" \
"$CAI_API/v1/agents/my-first-agent" | jq -r '.state, .message'
- Sign in at https://console.codyhill.dev, press Cmd+K / Ctrl+K to pick your project, then go to Compute → Agents and click Deploy agent.
- Name the agent
my-first-agent. Names must be lowercase letters, digits, and hyphens. - Pick the framework — ADK, CrewAI, or LangGraph.
- Choose the write mode and replace the starter file with the code from step 2.
- Click through Review & deploy.
You should see: a build panel streaming the status — building, then deploying, then ready. This usually finishes within a few minutes. If it lands on failed, the build's own output is shown right there on the agent's Overview tab — that is the build log.
Step 4: Talk to it
Send a message that makes the agent do arithmetic. It should reach for the sandbox rather than guess.
- platformctl
- curl
- Console
platformctl invoke my-first-agent "My favorite number is 42. Compute 2**32 in python."
You should see:
2**32 is 4294967296.
(session: 3f2c8a1e-9b7d-4e21-a6c0-5d8f13b2e470)
tool_call: run_python called with args={'code': 'print(2**32)'}
Copy that session id — you need it in step 5.
curl -s -X POST "$CAI_API/v1/agents/my-first-agent/invoke" \
-H "Authorization: Bearer $CAI_TOKEN" \
-H 'content-type: application/json' \
-d '{"message":"My favorite number is 42. Compute 2**32 in python."}'
You should see:
{
"session_id": "3f2c8a1e-9b7d-4e21-a6c0-5d8f13b2e470",
"user_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"output": "2**32 is 4294967296.",
"reasoning": "",
"tool_calls": [
{"name": "run_python", "summary": "called with args={'code': 'print(2**32)'}"}
],
"events": ["..."]
}
Copy the session_id — you need it in step 5. Invoking takes the same credential as every other call on this platform (INVOKE_AUTH_REQUIRED=true on this install) — send it without one and the platform answers 401 naming exactly that.
Open the agent's Test tab and type:
My favorite number is 42. Compute 2**32 in python.
You should see: a reply containing 4294967296, a tool call entry showing run_python ran, and the session id — the handle for this conversation.
Three things happened. The model decided to use a tool. run_python executed real Python in an isolated sandbox. And the platform minted a session id — the handle for this conversation.
Step 5: Prove it remembers
Ask a follow-up in the same session. Nothing in your new message mentions 42, so the only way the agent can answer is from the stored conversation.
- platformctl
- curl
- Console
platformctl invoke my-first-agent "What's my favorite number?" \
--session 3f2c8a1e-9b7d-4e21-a6c0-5d8f13b2e470
curl -s -X POST "$CAI_API/v1/agents/my-first-agent/invoke" \
-H "Authorization: Bearer $CAI_TOKEN" \
-H 'content-type: application/json' \
-d '{"message":"What'\''s my favorite number?",
"session_id":"3f2c8a1e-9b7d-4e21-a6c0-5d8f13b2e470"}' | jq -r .output
In the same Test panel, send a follow-up:
What's my favorite number?
The Test panel reuses the same session for you — there is nothing to copy.
You should see: a reply mentioning 42. The platform stored turn 1 and replayed it to the model for turn 2. That's sessions working. Start a turn without the session id and each invoke begins a brand-new conversation.
Step 6: Look under the hood (optional)
- platformctl
- curl
- Console
platformctl status my-first-agent
You should see: the agent's name, framework, state (ready), address, and image. The address is the agent's public URL, or private if you have not published it. The one field the table does not show is latest_revision — the next command lists revisions properly anyway.
To list this agent's revisions — immutable snapshots of your code and settings, one per deploy — and the share of traffic each one serves:
platformctl agents revisions my-first-agent
And to read logs:
platformctl logs my-first-agent --history
--history shows persisted logs, which survive even when the agent has scaled to zero.
GET /v1/agents/{name} returns more than the CLI table shows — including public_url:
curl -s -H "Authorization: Bearer $CAI_TOKEN" \
"$CAI_API/v1/agents/my-first-agent"
You should see:
{
"name": "my-first-agent",
"state": "ready",
"ready": true,
"kind": "agent",
"framework": "adk",
"image": "registry.us-east1-a.ccr.crusoecloudcompute.com/cai-ab12cd-my-first-agent@sha256:9f2c1a...",
"url": "http://<private-hostname>",
"public_url": "https://my-first-agent-ab12cd.apps.codyhill.dev",
"latest_revision": "my-first-agent-00001",
"owner": "you@example.com"
}
state is the agent's own word for where it is — building, deploying, ready, or failed — and ready is the boolean that means the same thing on every resource: you can use this right now. A message field joins them when something went wrong, and is left out when there is nothing to say.
The agent's own /debug/config and /healthz are open on that address once you publish it.
The agent's Overview tab shows its status, framework, image, public URL, and the build output. The Revisions tab lists every deploy — immutable snapshots of your code and settings — with the share of traffic each one serves. The Logs tab reads persisted logs, which survive even when the agent has scaled to zero.
Clean up
Optional — skip this if you want to keep going.
- platformctl
- curl
- Console
platformctl delete my-first-agent
You should see:
deleted my-first-agent
curl -s -X DELETE -H "Authorization: Bearer $CAI_TOKEN" \
"$CAI_API/v1/agents/my-first-agent"
On the agent's page, click Delete and confirm. The endpoint, its build, and its stored source are removed.
Delete is narrower than it looks: it removes the running agent, not the conversations it stored or anything it memorized. See what delete leaves behind before you rely on it to remove data.
What just happened
- Build. The platform packaged your folder and built it into a container image. The image is layered on top of your framework's managed harness, a small web server that runs your code. The finished image went into your own Crusoe Cloud Registry, into a repository the platform created for this agent and named
cai-<project-short>-<workload>— so it sits in your account, under your quota, alongside your other repositories. - Deploy. The image became a serverless service in your project with a stable invoke endpoint, pulling from that repository with a credential minted from the Crusoe Cloud key you connected in step 1.
- Wire-up. Sessions (conversation history), the memory bank, the sandbox, and the model were injected automatically — zero configuration from you.
- Scale-to-zero. A few minutes after your last message, the agent's instances drop to zero. The next invoke cold-starts it, so expect that first reply after an idle period to be slower than the ones that follow.
Notice what did not change between the three framework tabs in step 2: steps 3 through 6 are identical. Once deployed, an agent answers the same HTTP API no matter which framework wrote it — callers cannot tell them apart.
If something breaks
| Symptom | Cause and fix |
|---|---|
409 — this project cannot deploy yet: ... no Crusoe Cloud credential is mapped to this project | You skipped step 1. Nothing was built and your source was not touched. Connect the project, then deploy again. |
409 — this project has no inference credential | The project has no model API key, and the deploy was refused before building anything. Save the project's Crusoe Managed Inference key under Project Settings in the console (or PUT /v1/projects/{id}/inference), then deploy again. |
State is failed after deploy | The build or startup failed. Run platformctl status my-first-agent — the tail of the build output lands in the message field. In the console it is on the Overview tab. |
Error mentions could not import 'root_agent' | Your agent.py doesn't define a module-level root_agent. Match the code above exactly. The CrewAI and LangGraph equivalents are a missing crew and a missing graph. |
invalid agent name (must be a lowercase DNS label) | A DNS label is one piece of a hostname, the part between dots. Names allow only lowercase letters, digits, and hyphens, starting with a letter, up to 63 characters. |
| The reply is a model authentication error | The project's model key is wrong or was rotated without a redeploy. Check the key under Project Settings, then redeploy the agent so it picks the new value up — running instances read it at cold start. A per-agent MODEL_API_KEY overrides the project's key if set — see Secrets and env. |
401 — this platform requires authentication to invoke agents (INVOKE_AUTH_REQUIRED=true) | You invoked without a credential. Sign in (platformctl login) or send Authorization: Bearer $CAI_TOKEN — invoking is gated on this install, and the error names the setting in case your install differs. |
| Turn 2 forgot turn 1 | You didn't pass the session id, so each invoke started a fresh conversation. |
More in troubleshooting agents.
Next steps
- Crusoe Cloud integration — the rest of what the credential you connected in step 1 unlocks: object-storage buckets, your repository list, and the models your account can serve.
- Make it reachable by your users — publish the agent with
CAI_EXPOSE_EXTERNAL, because agents are private until you do. - Deploy your first function — a plain HTTPS endpoint in one file, in any of four languages.
- Sessions — how conversations persist, and the
user_idmodel. - Memory — promote a conversation into long-term memory that new sessions can recall.
- Tools — write your own tools beyond
run_python. - Framework guides with the full contract: ADK, CrewAI, LangGraph.
- Embedded chat widget — put this agent on your own website with one script tag.