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:
- The
manifestform field (text) or themanifest_fileupload ofPOST /v0/stacksorPOST /v0/stacks/{id}/deploy. See HTTP API. - 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#
| Key | Type | Default | Meaning |
|---|---|---|---|
name | string | see below | Display 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. |
ttl | duration | 1h | How 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. |
services | mapping | required | Service name to service spec. At least one and at most 4 entries. |
notify.webhook | URL | none | Every event of the
stack is POSTed here as JSON, signed with the stack's webhook_secret. See
Webhooks. |
claim_email | string | none | Only 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.
| Value | Seconds |
|---|---|
15m | 900 (the minimum) |
30m | 1800 |
1h | 3600 (the default) |
2700 | 2700 |
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.
| Key | Type | Default | Meaning |
|---|---|---|---|
path | string | . | Directory of the service, relative to the root of the upload. It must exist and must not resolve outside the upload. |
runtime | string | detected | python,
python-<version> (e.g. python-3.12), node or static. If
omitted, detected from the service directory with the same rules as inference. |
build | shell command | see Runtimes | Run in the service directory after dependencies are installed. |
start | shell command | see Runtimes | Starts the
process. It must listen on 127.0.0.1:$PORT. Not allowed on static services. |
output | string | dist, 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. |
health | path | none | Path 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. |
env | mapping | {} | Environment variables. Keys and values are converted to strings. Values can contain references. |
depends_on | string 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>#
- Creates a virtual environment in
.agentserve-venvinside the service directory, withuv venv(passing--python <version>when the runtime names one). Ifuvis not installed on the server, it falls back topython3 -m venvand the version is ignored. - Installs
requirements.txtif present, otherwise the project itself ifpyproject.tomlis present. - Runs
buildif given. - Default
start: the first ofmain.py,app.py,server.py,api.pythat containsFastAPI(orStarlette(starts asuvicorn <module>:app --host 127.0.0.1 --port $PORT; one that containsFlask(starts asflask --app <module> run --host 127.0.0.1 --port $PORT. If none matches, the build fails and asks for an explicitstart.
node#
- If there is a
package.json:npm ci --no-audit --no-fundwhenpackage-lock.jsonexists, otherwisenpm install --no-audit --no-fund. Installs run withNODE_ENV=developmentso devDependencies are installed. - Runs
build, ornpm run buildif the package has abuildscript, withNODE_ENV=production. - Default
start:npm start, if the package has astartscript. 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:
| Variable | Value |
|---|---|
PORT | The port the service must listen on. |
HOST | 127.0.0.1 |
DATA_DIR | Persistent per-service directory. Survives redeploys, expiry and claiming. See Data. |
AGENTSERVE | 1 |
AGENTSERVE_STACK | The stack id, e.g. stk_3f9a1c0b2d4e. |
AGENTSERVE_SERVICE | The service name. |
AGENTSERVE_URL | The service's own public URL. |
PYTHONUNBUFFERED | 1 |
NODE_ENV | The value from env if set, otherwise production. |
VIRTUAL_ENV | Python 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.
| Reference | Resolves to | Example |
|---|---|---|
${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. Astaticservice has no process, so itsinternal_urlandportdon't point at anything; use itsurl. - 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:
package.jsonpresent:staticif it has nostartscript and depends onvite,astro,parcelorreact-scripts(independenciesordevDependencies); otherwisenode.requirements.txt,pyproject.toml,main.pyorapp.pypresent:python.index.htmlpresent: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.
| Message | Cause |
|---|---|
manifest is not valid YAML/JSON: … | Parse error. |
manifest must be a mapping | The document is a list or a scalar. |
manifest needs a non-empty 'services' mapping | services missing, empty or
not a mapping. |
at most 4 services per unclaimed stack | More 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 letter | Invalid service name. |
service 'X': add-ons (postgres, redis) are not supported in this version yet | An
addon key. |
service 'X': path escapes the uploaded source | path resolves outside the
upload. |
service 'X': path 'P' not found in the uploaded source | The directory doesn't exist. |
service 'X': runtime 'R' must be one of python[-3.x], node, static | Unknown runtime, or none given and none detected. |
service 'X': env must be a mapping | env 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 -> a | Circular 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 900s | ttl 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.