Skip to content

Deploy and operate

An agent in production is a binary and a folder. There is no control plane to stand up and nothing to migrate; the operational surface is a process, a directory, and a set of environment variables.

The agent folder is deployment input, exactly like source. Put it in a git repository, copy it to the host, and point the binary at it:

Terminal window
maiden serve /srv/my-agent --port 8080

Alongside the folder, maiden writes <dir>/.maiden/state — the durable thread checkpoints. That directory is your data. It must live on a persistent volume, and it must survive a redeploy, or every conversation restarts from nothing. Use --state to put it somewhere else:

Terminal window
maiden serve /srv/my-agent --port 8080 --state /var/lib/maiden/state

maiden reads credentials from the environment and never from the agent folder. Two conventions:

  • Provider keys: OPENAI_API_KEY, ANTHROPIC_API_KEY.
  • Connection credentials: ${ENV_VAR} inside a connection’s headers or base_url, expanded from the environment when the agent loads.
  • Tool credentials: MAIDEN_SECRET_<NAME> for a name listed in a tool’s secrets = [...]. The tool only ever sees the name; the host resolves the value and attaches it. A tool can only name credentials its own policy declared, so an unrelated MAIDEN_SECRET_* in the environment is not reachable from it — see the security model.

Channel configs follow the same rule — they name an env var (token_env, signing_secret_env), never a literal. Keep the values in your platform’s secret store and inject them at process start.

Nothing in the state directory should contain a credential. If you find one there, that is a bug worth reporting.

maiden serve binds 127.0.0.1 by default and has no authentication. That default is deliberate: the front door is meant to sit behind something that already knows how to authenticate.

  • Behind a proxy — bind locally, and let nginx/Caddy/your gateway terminate TLS and authenticate.
  • In a container — set MAIDEN_BIND=0.0.0.0 (the published image does), and publish the port only to your internal network.

Webhook channels are the exception: they must be reachable by the provider, and they authenticate themselves by verifying the provider’s signature on every request. Give them a public path through your proxy; do not put a shared password in front of them, or the provider’s own verification handshake will fail.

A minimal systemd unit:

[Unit]
Description=maiden agent
After=network-online.target
[Service]
ExecStart=/usr/local/bin/maiden serve /srv/my-agent --port 8080 --state /var/lib/maiden/state
Environment=MAIDEN_BIND=127.0.0.1
EnvironmentFile=/etc/maiden/secrets.env
User=maiden
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target

Restart=always is safe. A restart mid-conversation is not a data-loss event — the next message on that thread resumes from the last checkpoint. This is the same property that makes deploys uneventful: stop the old process, start the new one, and threads continue.

  1. Install the new binary.
  2. Restart the process.

Checkpoints are forward-compatible within a minor version. maiden is pre-1.0, so a minor bump may change the state format — the changelog says so when it does. If you need certainty across an upgrade, back up the state directory first; it is plain JSON.

maiden emits one tracing span per model-call turn. It does not ship a subscriber — install your own and the spans go wherever you already send telemetry:

tracing_subscriber::fmt::init(); // or an OTLP layer

The span is maiden.model_call, carrying maiden.session.id, maiden.turn.index, maiden.tool.count and maiden.model. The most useful signal in it is turn index pressing max_turns — an agent hitting its ceiling is looping, not thinking.

Token spend is bounded separately by max_session_tokens, and a subagent’s spend is charged back to its parent, so a runaway nested agent shows up against the top-level budget rather than hiding one level down.

For failure, watch the process exit code and stderr as you would any service.

maiden serve also runs the folder’s schedules/ and channels/. Two consequences worth planning for:

  • Run exactly one process per agent folder, or a schedule fires once per process and a Telegram long-poll fights itself over the same offset.
  • Schedules have no catch-up. A schedule that would have fired during downtime does not fire on restart; the next occurrence is computed from the time the process starts. If a missed run matters, make the prompt idempotent and reconcile from state rather than relying on the tick.