Skip to content

Build your first agent

By the end of this tutorial you will have built a working agent from an empty directory: run it offline with no API key, give it a sandboxed tool, serve it over HTTP, and add a deterministic check. It takes about ten minutes.

The maiden binary on your PATH, and nothing else — no API key, no Docker, no Rust toolchain:

Terminal window
curl -fsSL https://maiden.sh/install.sh | sh

See Install for the cargo, npm and Docker channels, and for how to verify the download.

An agent is a directory, and maiden new writes one that already works:

Terminal window
maiden new my-agent
my-agent/
.gitignore thread state and tool build output
agent.toml model and per-tool policy
instructions.md the system prompt
skills/tone.md extra guidance appended to it
tools/greet.wasm a sandboxed tool, already built
tools/src/greet/ its source
evals/greet.toml a check that passes with or without a key

That is the whole agent — seven files you can read, diff, and commit. Nothing is hidden in a database or a dashboard, and there is no project to initialize.

Two of them are the ones an agent cannot run without. agent.toml says which model to call and how far to let it go:

[agent]
model = "openai/gpt-5.5"
instructions_file = "instructions.md"
max_turns = 6

And instructions.md is the system prompt, as plain prose:

You are a concise assistant.
When the user asks you to greet, echo, or repeat something, you MUST call the
`greet` tool with that text as the `input` argument — do not answer from
memory. After it returns, quote what it returned rather than paraphrasing it.

You don’t need an API key to see the loop work. The --mock flag swaps in a scripted provider that behaves deterministically:

Terminal window
maiden run my-agent "greet the world" --mock
[maiden] agent 'my-agent' tools [greet]
[maiden] 2 turn(s)
Tool returned: {"echoed":"from-mock"}

Two turns: the provider called greet, then wrote an answer quoting what came back. The tool ran inside the WASM sandbox with a timeout and a memory ceiling, and you didn’t configure any of it.

The eval that came with the folder passes too:

Terminal window
maiden eval my-agent --mock
ok greet
[maiden] 1/1 eval(s) passed

greet.wasm shipped prebuilt so that step 2 worked on a machine with no Rust toolchain, but its source is right there in tools/src/greet/src/lib.rs. Open it and you will find three functions: describe tells the model when to reach for the tool, schema declares the arguments it accepts, and execute does the work.

Make it shout:

Ok(serde_json::json!({ "echoed": input.to_uppercase() }).to_string())

Rebuilding needs cargo-component:

Terminal window
maiden tool build greet --dir my-agent
maiden run my-agent "greet the world" --mock --thread t2
Tool returned: {"echoed":"FROM-MOCK"}

Note the --thread t2. Without it the run resumes the thread you started in step 2, and a resumed thread already has its answer — you would see the old from-mock and think the rebuild failed. That is durability doing its job, and it is the same reason maiden eval runs on throwaway state.

The tool name comes from the path — tools/greet.wasm is the tool greet. Rename the file and you have renamed the tool; there is no registry to update. To write a second one from scratch, see Add a sandboxed tool.

Drop --mock to run against the model in agent.toml. That model is an OpenAI model, so set your key:

Terminal window
export OPENAI_API_KEY="sk-..."
maiden run my-agent "greet the world, then explain what you did"

Now a real model decides whether greet is the right tool and writes the answer in its own words — and skills/tone.md, which was appended to the system prompt all along, is what keeps it to three sentences. To use Anthropic instead, change model in agent.toml — see Choose a provider.

Instead of one-shot runs, serve the agent over HTTP. Each thread is checkpointed after every turn, so conversations survive restarts.

Terminal window
maiden serve my-agent --port 8080

In another terminal:

Terminal window
curl -s localhost:8080 -d '{"thread":"t1","message":"greet the world"}'

Send a second message on the same thread and it continues the conversation. Kill the server, start it again, and thread t1 resumes from disk. To stream the answer token by token, see Serve over HTTP and stream.

You ran evals/greet.toml back in step 2. An eval is a prompt plus what must be true of the run, so adding one is adding a file. Create my-agent/evals/shouts.toml to pin the change you made in step 3:

prompt = "greet the world"
expect = ["FROM-MOCK"]

FROM-MOCK is the scripted provider’s own argument, shouted back — so unlike greet.toml, this case only passes with --mock. That is the tradeoff worth knowing: assert on answer text and you pin one provider’s wording; assert on expect_count (as greet.toml does) and the case holds against both.

Terminal window
maiden eval my-agent --mock
ok greet
ok shouts
[maiden] 2/2 eval(s) passed

maiden eval exits non-zero if any case fails, so this line drops straight into CI. See Write and run evals for more.

You’ve touched every core piece. To go deeper: