Docs / Reference

agentserve.yaml

Every field of the stack manifest, the runtimes, ${svc.*} references, inference without a manifest, and validation errors.

The manifest describes the services in a stack: where each one lives in the upload, how to build it, how to start it and which environment it gets. It is optional. Without one, AgentServe infers the services from the files it finds.

name: todo-app
ttl: 1h
claim_email: [email protected]
notify:
  webhook: https://example.com/hooks/agentserve
services:
  api:
    path: ./backend
    runtime: python-3.12
    start: uvicorn main:app --host 127.0.0.1 --port $PORT
    health: /healthz
    env:
      CORS_ORIGINS: ${web.url}
  web:
    path: ./frontend
    runtime: static
    build: npm run build
    output: dist
    env:
      VITE_API_URL: ${api.url}
    depends_on: [api]

Where the manifest comes from#

The server looks for a manifest in this order:

  1. The manifest form field (text) or the manifest_file upload of POST /v0/stacks or POST /v0/stacks/{id}/deploy. See HTTP API.
  2. A file at the root of the uploaded archive, checked in this order: agentserve.yaml, agentserve.yml, agentserve.json.

The file is parsed as YAML, so JSON works too. An empty file counts as an empty mapping, which then fails validation because it has no services. If no manifest is found at all, the services are inferred.

Top-level keys#

KeyTypeDefaultMeaning
namestringsee belowDisplay name of the stack. Also the base of the stack's slug, which appears in every service hostname (<service>-<slug>.<domain>). A name sent with the request overrides it.
ttlduration1hHow long an unclaimed stack runs before it expires. Read only when the stack is created; a ttl sent with the request overrides it. See Durations.
servicesmappingrequiredService name to service spec. At least one and at most 4 entries.
notify.webhookURLnoneEvery event of the stack is POSTed here as JSON, signed with the stack's webhook_secret. See Webhooks.
claim_emailstringnoneOnly an account with this email address (case-insensitive) can claim the stack. A claim_email sent with the request takes precedence. Read only when the stack is created.

Default name. If neither the manifest nor the request gives a name, the server uses the name of the directory it extracted the upload into, which is always src. The CLI and the MCP server avoid this by sending the project directory's name when the project has no manifest file; with a manifest file and no name key, the stack is named src. Set name explicitly.

Redeploys. On a redeploy the stack keeps its original name, TTL and claim_email. The new manifest's services replace the old ones. A notify.webhook that differs from the current one replaces it; removing the key does not remove the existing webhook. See Redeploys.

Unknown top-level keys are ignored.

Durations#

A duration is an integer followed by an optional unit: s, m, h or d. Whitespace between the number and the unit is allowed and case is ignored. A bare number, or a YAML integer, is a number of seconds.

ValueSeconds
15m900 (the minimum)
30m1800
1h3600 (the default)
27002700

Compound values such as 1h30m are rejected. Stacks deployed with an API key go straight into an account and never expire. A ttl on such a stack is still validated, but not enforced. See Claiming.

Service keys#

Service names must match ^[a-z][a-z0-9]{0,19}$: lowercase letters and digits, starting with a letter, at most 20 characters, no hyphens. The name is the first part of the service's hostname.

KeyTypeDefaultMeaning
pathstring.Directory of the service, relative to the root of the upload. It must exist and must not resolve outside the upload.
runtimestringdetectedpython, python-<version> (e.g. python-3.12), node or static. If omitted, detected from the service directory with the same rules as inference.
buildshell commandsee RuntimesRun in the service directory after dependencies are installed.
startshell commandsee RuntimesStarts the process. It must listen on 127.0.0.1:$PORT. Not allowed on static services.
outputstringdist, then build, then .static only. Directory, relative to the service directory, that holds the built site. It must contain index.html. The default is the first of dist and build that exists after the build, otherwise the service directory itself.
healthpathnonePath polled after start, e.g. /healthz. With health set, the service is healthy once it answers with a status below 300. Without it, GET / must answer with a status below 500. Either way it has 60 seconds by default.
envmapping{}Environment variables. Keys and values are converted to strings. Values can contain references.
depends_onstring or list[]Services that are built and started before this one. Every entry must be a service in the same manifest, and the graph must not have cycles.

A service with an addon key is rejected: managed add-ons (Postgres, Redis) are not supported yet. Other unknown keys are ignored.

Runtimes#

Every build and start command runs through /bin/sh -c in the service directory. Each build command has a 600-second timeout by default. Output goes to the service's build log (agentserve logs --build).

python, python-<version>#

  1. Creates a virtual environment in .agentserve-venv inside the service directory, with uv venv (passing --python <version> when the runtime names one). If uv is not installed on the server, it falls back to python3 -m venv and the version is ignored.
  2. Installs requirements.txt if present, otherwise the project itself if pyproject.toml is present.
  3. Runs build if given.
  4. Default start: the first of main.py, app.py, server.py, api.py that contains FastAPI( or Starlette( starts as uvicorn <module>:app --host 127.0.0.1 --port $PORT; one that contains Flask( starts as flask --app <module> run --host 127.0.0.1 --port $PORT. If none matches, the build fails and asks for an explicit start.

node#

  1. If there is a package.json: npm ci --no-audit --no-fund when package-lock.json exists, otherwise npm install --no-audit --no-fund. Installs run with NODE_ENV=development so devDependencies are installed.
  2. Runs build, or npm run build if the package has a build script, with NODE_ENV=production.
  3. Default start: npm start, if the package has a start script. Otherwise the build fails.

A version suffix such as node-20 passes validation but is ignored: the server's Node is used.

static#

Installed and built like node, if there is a package.json. The output directory is then served by AgentServe itself; there is no process. Requests for paths without a file extension that don't match a file get index.html, so client-side routes work. Missing files with an extension return 404. index.html is sent with Cache-Control: no-cache, everything else with public, max-age=300.

Environment#

A process gets PATH, HOME, LANG, TMPDIR, USER and SHELL from the server's environment, then the service's resolved env, then these variables, which override anything with the same name in env:

VariableValue
PORTThe port the service must listen on.
HOST127.0.0.1
DATA_DIRPersistent per-service directory. Survives redeploys, expiry and claiming. See Data.
AGENTSERVE1
AGENTSERVE_STACKThe stack id, e.g. stk_3f9a1c0b2d4e.
AGENTSERVE_SERVICEThe service name.
AGENTSERVE_URLThe service's own public URL.
PYTHONUNBUFFERED1
NODE_ENVThe value from env if set, otherwise production.
VIRTUAL_ENVPython services only. Its bin directory is prepended to PATH.

The same environment is used for build commands, so a frontend build can read VITE_API_URL and bake it into the bundle.

References#

Values in env can refer to any service in the same stack, including services declared later and the service itself. References are resolved before every build and every start.

ReferenceResolves toExample
${svc.url}Public URL of the service.https://api-todo-app-3f9a.agentserve.sh
${svc.host}Public hostname, without scheme or port.api-todo-app-3f9a.agentserve.sh
${svc.internal_url}Loopback URL of the service's process.http://127.0.0.1:53817
${svc.port}The service's local port.53817
  • Use ${svc.url} for anything a browser calls (a frontend's API URL, CORS origins).
  • Use ${svc.internal_url} for server-to-server calls. A static service has no process, so its internal_url and port don't point at anything; use its url.
  • Local ports are assigned by the server and can change when a port is taken at start-up. Public URLs don't change.
  • Only the four fields above are references. Any other ${...} text is left as is; a reference to a service that doesn't exist is a validation error.

Inference without a manifest#

A directory's runtime is detected with these rules, in order:

  1. package.json present: static if it has no start script and depends on vite, astro, parcel or react-scripts (in dependencies or devDependencies); otherwise node.
  2. requirements.txt, pyproject.toml, main.py or app.py present: python.
  3. index.html present: static.

If the root of the upload matches, the stack has one service at path: ., named web for a static site and app otherwise. If the root doesn't match, each top-level subdirectory not starting with . is checked, and every match becomes a service named after the directory (lowercased, with non-alphanumeric characters removed, at most 20 characters). Inferred stacks have no env, depends_on or health, so services can't reference each other: write a manifest for that.

my-project/
  backend/     requirements.txt      →  service "backend", runtime python
  frontend/    package.json (vite)   →  service "frontend", runtime static

Validation errors#

An invalid manifest rejects the deploy with HTTP 422 and {"detail": "<message>"}. On a redeploy, nothing changes and the current generation keeps serving.

MessageCause
manifest is not valid YAML/JSON: …Parse error.
manifest must be a mappingThe document is a list or a scalar.
manifest needs a non-empty 'services' mappingservices missing, empty or not a mapping.
at most 4 services per unclaimed stackMore than 4 services. The same limit applies to stacks deployed with an API key, despite the wording.
service 'X' must be lowercase letters/digits, starting with a letterInvalid service name.
service 'X': add-ons (postgres, redis) are not supported in this version yetAn addon key.
service 'X': path escapes the uploaded sourcepath resolves outside the upload.
service 'X': path 'P' not found in the uploaded sourceThe directory doesn't exist.
service 'X': runtime 'R' must be one of python[-3.x], node, staticUnknown runtime, or none given and none detected.
service 'X': env must be a mappingenv is a list or a scalar.
service 'X' depends on unknown service 'Y'Bad depends_on entry.
service 'X': env references unknown service ${Y...}A reference to a service that isn't in the manifest.
service 'X': static services are served by AgentServe, drop 'start'start on a static service.
depends_on has a cycle: a -> b -> aCircular dependencies.
no manifest given and nothing deployable detected (…)No manifest and inference found nothing.
ttl '…' is not a duration like '30m', '1h' …Unparseable ttl.
ttl must be at least 900sttl below 15 minutes.
source must be a tar or tar.gz archive: …The upload isn't a tar archive.
archive entry escapes the source root: …An entry with .. or an absolute path.

Build and start failures are not validation errors: the stack becomes failed (first deploy) or stays live with deploy_error set (redeploy). Read them with agentserve logs --build.

Stuck? Your agent can read /skill.md, or connect it over MCP.