Skip to content

astrolift.toml reference

Agent notes

The registered app's manifest is what the platform runs, not your working copy: astro app manifest pull <app-slug> replaces the staged draft with the repo's file, and astro ci render prints the Kubernetes resources the server renders from it. The parser is backend/astrolift_manifest/parser.py in astrolift-app; [package], [environment] and [secrets] on an agent manifest are read by backend/astrolift_agents/services/agent_package.py. Compatibility only: scope, tier, version and inject_as on a managed service, web and worker as [app] kind, dockerfile on a job or task. Frequent mistakes: an unknown key inside [[workloads]] or a container is refused, so env goes under [workloads.containers.env], not on the workload; a cronjob without schedule; [workloads.metrics] without port; [app] slug or display_name missing, which astro app register refuses before contacting the server.

astrolift.toml is used by more than one source workflow. Identify the file's role before choosing fields:

Role Discovery path Required shape
App astrolift.toml or apps/<slug>/astrolift.toml top-level name; one or more non-agent [[workloads]]
Agent astrolift.toml or agents/<slug>/astrolift.toml top-level name; exactly one kind = "agent" workload
Agent config/brief include selected by an environment spec [skills.*], [environment], [secrets], optional include

Workflow definitions use workflows/**/*.toml, not astrolift.toml; see the workflow reference. An agent manifest can additionally use the Agent Package fields in Agent packages.

Minimal application

astrolift_version = 1
name = "orders-api"

[environments.production]

[app]
slug = "orders-api"
display_name = "Orders API"

[[workloads]]
name = "web"
kind = "deployment"
is_public = true
replicas = 2
cpu_request = "250m"
cpu_limit = "1"
memory_request = "256Mi"
memory_limit = "512Mi"
hpa_min = 2
hpa_max = 10
hpa_target_cpu_pct = 70

  [[workloads.containers]]
  name = "web"
  is_primary = true
  dockerfile_path = "Dockerfile"
  build_context = "."
  port = 8000

    [workloads.containers.env]
    LOG_LEVEL = "info"

    [workloads.containers.healthcheck]
    kind = "http"
    value = "/health"
    port = 8000

The top-level name is the server parser's required display name. The [app] identity is read by the CLI registration flow; include both. Unknown keys inside [[workloads]], [[workloads.containers]], [[managed_services]], [edge], [ingress.access] and [brain] are refused at parse time with the offending key path. An unknown top-level table is kept with the manifest but affects nothing.

There is not yet a generated JSON Schema for this filename. App manifests and agent configuration/includes are two related parser families, and validation also depends on cross-field rules, source paths, chroot boundaries, provider capabilities, and organization policy. Treat target-server validation as authoritative until those parsers consume shared typed contracts that can emit separate app and agent schemas plus a dispatcher schema.

[environments.<name>] tables declare environment names that registration and repo resync can bootstrap, as the empty production table above does. Manage the environment's cluster binding and runtime configuration through the control plane; arbitrary nested override keys are not a supported deployment contract yet.

[app]

Key Read by Meaning
slug astro app register The app slug sent to registration; required by the CLI
display_name astro app register The registered name; required by the CLI
kind server What the app is for, separate from how it runs. Omit it for a generic app. The one registered kind is mimwell, a brain-serving app; the older web and worker values are still accepted and mean a generic app
expires_in both Makes the app temporary: 30m, 4h, 7d, 6w or 3mo (30 days) from registration. The organization's temporary-app policy applies; see Temporary apps

[brain]

[brain]
slug = "handbook"

slug names the hosted brain this app reads. The manifest only names it; the grant that lets the app read the brain is made by someone who can grant it. slug is the table's only key.

[env]

[env]
LOG_FORMAT = "json"
FEATURE_FLAGS = "beta-search"

App-wide literals, materialized into a per-environment Kubernetes Secret the app's containers read. Precedence from lowest to highest: [env], app secret bundles, managed-service connection envelopes, then [workloads.containers.env]. A document still carrying the masked placeholder from a masked read is refused wherever it is applied, so a secret is never replaced by the mask.

Workloads

Every [[workloads]] requires name and kind. Names must be unique across workloads, [[jobs]], and [[tasks]].

Supported kinds are deployment, statefulset, job, cronjob, task, agent, workflow, function, static_site, and faas.

Common fields:

Field Default Meaning
is_public false Create the public service/hostname path
replicas 1 Fixed replica count when HPA is not active
cpu_request, cpu_limit platform defaults Kubernetes quantities
memory_request, memory_limit platform defaults Kubernetes quantities
hpa_min, hpa_max unset HPA bounds
hpa_target_cpu_pct 80 HPA CPU target
storage_class, storage_size unset Legacy workload storage defaults
gpu 0 Whole GPUs (0 to 16) requested on the primary container, or MIG slices when mig_profile is set
gpu_type unset Pins the GPU product label, such as nvidia-l4; needs gpu >= 1
mig_profile unset A MIG slice such as 3g.47gb; needs gpu >= 1
fs_group platform default Pod fsGroup for a workload that mounts a PVC. Falls back to run_as_user from [workloads.security], then to the platform default. Emitted only when the workload mounts a PVC

A cronjob additionally requires schedule and accepts concurrency_policy = "forbid" | "queue" | "replace" (default forbid).

Metrics

A workload is not scraped unless it says so. Declaring the table is the opt-in; port is required because a scrape target with no port would have to be guessed.

[[workloads]]
name = "web"
kind = "deployment"
is_public = true

  [workloads.metrics]
  port = 9090
  path = "/metrics"   # default
  # enabled = false   # keep the config, stop the scrape
Field Default Meaning
port required Container port serving the metrics endpoint
path /metrics Path scraped on that port
enabled true false keeps the table and turns the scrape off

Rendering emits both scrape mechanisms, because a cluster may run either: prometheus.io/scrape, prometheus.io/port and prometheus.io/path annotations on the pod, and a PodMonitor scoped to the app's namespace for a Prometheus Operator install. A private worker gets a PodMonitor with no Service, which is the case annotation-only scraping handles badly.

What the request panels need

Traffic, errors and latency (the RED half of the golden signals) are read from one of two sources, and an app that matches neither leaves those three panels empty:

  • Edge metrics. If the cluster's ingress controller is one Astrolift has a mapping for, the request signals come from the controller's own metrics, keyed by the app's namespace. Nothing is required of the app.
  • Application metrics. Otherwise the queries read the Prometheus HTTP conventions from the app itself:
Signal Metric Type
Traffic, errors, status codes http_requests_total counter, with a code label
Latency quantiles http_request_duration_seconds_bucket histogram

Both must carry an app="<app-slug>" label and, to answer the per-environment and per-workload views, environment and workload labels matching those slugs. The standard Prometheus client library for your language emits both metrics under these names; the labels are yours to add.

Saturation (CPU and memory) is read from the cluster's own cAdvisor and kube-state-metrics series, so it populates whether or not the app is instrumented. When the request panels have no source at all, they say so rather than reporting an empty window.

Edge identity

Apps behind the platform's auth gate receive the authenticated identity as X-Auth-Request-User / X-Auth-Request-Email headers, and a shared secret on X-Astrolift-Gateway-Secret that proves the request came through the gate rather than from anything else that can reach the Service port in-cluster.

An app whose backend reads those under names of its own declares the mapping:

[edge]
gateway_secret_header = "x-qsr-proxy-secret"

[edge.identity_headers]
user = "x-qsr-user-id"
email = "x-qsr-email"
Key Meaning
gateway_secret_header Header the gate's shared secret is stamped on for this app. The value is never declared here; it comes from the cluster's auth config
identity_headers identity = header-name pairs. Valid identities are user, email and access_token, the ones the gate forwards

The block is app-level, not per workload: every managed hostname for an app shares one Ingress.

Header names must be valid HTTP header names; the value is written into the ingress controller's configuration, so anything else is refused at parse time. An identity the gate does not forward is refused too; a mapping for a header that never arrives would render configuration that silently forwards nothing.

This is deliberately a mapping rather than a raw configuration snippet. A snippet ties the app to one ingress controller's config language and hands it a sharp edge, and ingress-nginx has been narrowing allow-snippet-annotations. If your app needs something the mapping cannot express, that case is worth filing rather than working around.

Ingress and access

[ingress]
class = "nginx"

[ingress.auth]
kind = "oidc"
auth_proxy_host = "auth.apps.example.com"
discovery_url = "https://login.example.com/.well-known/openid-configuration"
client_id = "astrolift-edge"

[ingress.access]
groups = ["clinical-ops"]
users = ["oncall@example.com"]

[ingress] and [ingress.auth]

The login gate is a cluster setting, not an app setting. A cluster on the ALB ingress class gates with Cognito; other classes gate with the cluster's central OIDC proxy. Every managed hostname on a gated cluster sits behind the gate.

[ingress] and [ingress.auth] are the platform's record of that gate in the app's repo. When an operator changes a cluster's gate, Astrolift AI writes the matching table into the manifest of every app on that cluster, as a commit on the deploy branch, or as a pull request when the branch is protected. The renderer reads the cluster's configuration, not this table.

kind Fields
cognito user_pool_arn, user_pool_client_id, user_pool_domain
oidc auth_proxy_host, discovery_url, client_id, optional logout_url

[ingress] carries class, the cluster's ingress class. The OIDC cookie secret is never written to the manifest.

Editing or removing [ingress.auth] does not change the gate. An app whose manifest has no [ingress.auth] is still gated while its cluster is, and the platform writes the table back the next time the cluster's gate changes. To take an app off the gate, change the cluster, not the manifest.

[ingress.access]

[ingress.access] narrows who may enter once signed in. groups are identity provider groups, users are email addresses. A signed-in user outside both lists gets a no-access page, never the app. Empty lists restrict nobody.

The rule is enforced on clusters using the Envoy edge. On any other edge the app is gated by login only, and astro app access show says the rule is not enforced yet.

astro app access manages the same rule from the CLI. my-app is the app slug from astro app list:

astro app access show my-app
astro app access allow my-app --group clinical-ops
astro app access deny my-app --user former@example.com
astro app access clear my-app

show prints my-app: open to every signed-in user of the cluster's central auth. for an open app, or my-app: only these may enter. followed by the groups: and users: lines. allow and deny print how many users would lose access and wait for y; when the manifest declares [ingress.access] they are refused, and the fix is to edit the manifest and sync it.

Manifest state Effect
[ingress.access] declared The manifest's lists replace the rule on every sync. show reports it as set by the manifest; allow, deny and clear are refused
Removed from a manifest that declared it The rule is cleared on the next sync: every signed-in user may enter. astro app access can set a rule again
Never declared A rule set with astro app access or the dashboard is left alone

allow on an open app restricts it to what you name. allow and deny preview who would lose access and ask before saving; pass --yes to skip the question.

[ingress] max_body_size

The largest request body the edge passes to the app.

[ingress]
max_body_size = "100m"

The value is a byte count with an optional k, m or g suffix, the way nginx writes a size, or "0" for no limit. Without the key there is no limit. A value nginx cannot read, such as "100mb" or "1.5m", is refused when the manifest is parsed. The key is the app's own: when the platform writes [ingress.auth] back into the manifest, it leaves max_body_size in place. A change takes effect on the app's next deploy.

On an ingress-nginx edge the value becomes the app Ingress's nginx.ingress.kubernetes.io/proxy-body-size, and a larger body gets a 413 from the edge. Whatever the limit, the body streams to the app as it arrives instead of being stored at the edge first (proxy-request-buffering: "off"), and the edge allows the app an hour between reads or writes on its connection (proxy-read-timeout, proxy-send-timeout) instead of nginx's 60 seconds, so a slow upload or a slow response is not cut off.

The Envoy edge and the ALB set no body limit, and the key has no effect there.

Containers

[[workloads.containers]] supports name, is_primary, image_ref, dockerfile_path (default Dockerfile), build_context (default .), port, command, args, and [workloads.containers.env].

Health checks are structured:

[workloads.containers.healthcheck]
kind = "http" # none | http | tcp | exec
value = "/health"
port = 8080

When no container is explicitly primary, normalization selects the first. An explicit primary is clearer. A portless worker does not receive a fabricated HTTP probe; a container with a port and no healthcheck gets the platform's default HTTP probe on /health.

dockerfile_path and build_context must be repo-relative; an absolute path is refused. Only one image is built per app, so two containers declaring different non-default values for either field are refused at parse time.

Jobs and tasks shorthand

[[jobs]]
name = "nightly-report"
schedule = "0 3 * * *"
concurrency_policy = "forbid"
dockerfile_path = "Dockerfile.jobs"
command = ["python", "-m", "jobs.report"]

  [jobs.env]
  REPORT_MODE = "full"

[[tasks]]
name = "reindex"
dockerfile_path = "Dockerfile.jobs"
command = ["python", "-m", "jobs.reindex"]

[[jobs]] becomes a single-container cronjob. [[tasks]] becomes a single-container one-shot Job and has no schedule. Both accept name, image_ref, dockerfile_path (or its alias dockerfile), build_context, command, args, env, and the cpu_* and memory_* quantities; a job also requires schedule and accepts concurrency_policy. Neither exposes a port or carries a probe.

Volumes

[[workloads.volumes]]
name = "data"
kind = "pvc" # pvc | empty_dir | config_map | secret
mount_path = "/data"
size = "20Gi"
storage_class = ""
access_mode = "ReadWriteOnce"
performance_tier = "balanced"
durability = "zonal"

PVC volumes require size. config_map and secret require source_name. empty_dir may set size_limit. Paths must be absolute mount paths. performance_tier is standard, balanced (default), high_iops or extreme; durability is local, zonal (default) or regional.

Managed services

[[managed_services]]
kind = "postgres"
name = "primary"
variant = "rds"

  [managed_services.config]
  version = "17"
  size = "small"

Choose a non-empty variant advertised by the target cluster catalogue; rds is an example and requires a configured AWS provider.

Declaring a service provisions it. When the manifest is applied (at astro app register, and on every later sync from the repo), each entry is reconciled into a managed service and a provisioning run starts. The parser itself provisions nothing; the reconcile that follows it does, once the app has an environment bound to a cluster. Provisioning is asynchronous, so a registration can return before the database exists.

Field Default Meaning
kind required Service kind from the cluster catalogue, such as postgres or redis
name the kind Name, unique per kind and environment
variant catalogue default Provider variant, such as rds
owner_scope app app: the app owns the service. project: a shared project resource the app attaches to
environment production App environment the service belongs to. Registration creates it; a later sync refuses an environment that does not exist
bind_workloads ["*"] Workloads that receive the binding
size small small, medium, large, xlarge or custom (tier is an accepted alias)
config {} Provider configuration table, such as version. The top-level version and inject_as keys are accepted and folded into it
isolation catalogue default shared or dedicated
auth_mode catalogue default password, iam or mtls
extensions {} A string array of extensions to enable, or a provider-extension table
networking, retention, backup {} Provider option tables
restore {} Restores from a snapshot; requires both snapshot_id and source_handle
deletion_policy retain delete also requires confirm_delete = true

scope is an accepted alias for owner_scope. Any of these tables may carry secret references; a key named like a secret value (password, token, api_key, connection_string, and so on) is refused unless its name ends in _ref, _name, _id or _path. Anything else is refused as unknown; provider-specific options belong in extensions.

With owner_scope = "app" the service belongs to this app and environment, and appears under astro app services. Removing the entry from the manifest deprovisions it; provider data is deleted only under deletion_policy = "delete". The manifest cannot take over a service of the same kind and name that was created outside it; the sync fails instead.

With owner_scope = "project" the entry creates the project resource if the project has none of that kind and name, then attaches this app environment to it. An existing project resource must match the entry's variant and configuration; an app manifest cannot change a shared resource. Creating or attaching a project resource this way needs project.update on the project, the same permission astro project resources attach needs. Without it, a --manifest-raw registration is refused, and other registrations and syncs leave those changes out. Removing the entry detaches the app environment; the project resource stays. See Shared services for the CLI path.

Bound services reach workloads through stable environment envelopes such as DATABASE_URL and REDIS_URL. Do not put credentials in [workloads.containers.env]; use secret bundles, managed-service bindings, or agent secret references.

Kind-specific fields

Agent workload fields are run_family = "task" | "service", max_retries (default 5), tool_timeout_seconds (300), and result_ttl_hours (72). run_mode declares how the agent is triggered: once, loop, schedule, trigger or persistent; schedule requires run_cron_expression, which is validated as a cron expression at parse time. Leaving run_mode out keeps whatever the registration or an operator set. brains lists the hosted brains (by slug, at most 10) every run reads; brains_allowed lists the brains a launch may add (at most 100). Without brains_allowed a launch may add any brain the launcher can read; an empty list allows none. All of these are refused on a non-agent workload.

A workflow-worker workload requires workflow_type and task_queue; it also accepts temporal_namespace, max_concurrent_activities, and max_concurrent_workflows.

A Knative function accepts min_scale, max_scale, concurrency, and timeout_seconds.

A static_site has no containers. With platform build mode, set both static_build_command and static_output_dir; optional fields are static_spa and static_index.

A faas has no containers. faas_package_type = "image" must not set runtime or handler. "zip" requires faas_runtime, faas_handler, and faas_output_dir. Other fields include faas_memory_mb, faas_timeout_seconds, faas_architecture, faas_public, and faas_build_command.

Monorepos

Astrolift recognizes only the bounded declarative layouts:

apps/<slug>/astrolift.toml
agents/<slug>/astrolift.toml
workflows/**/*.toml

Agent package [package] rules create a separate chrooted source slice. Use explicit --manifest-path selections to register only part of a large repo.

The image build context is an app setting, and its default depends on how the app was registered. Whole-repo discovery sets it to the manifest's directory. astro app register --manifest-path apps/web/astrolift.toml leaves it at the repository root unless you also pass --build-context apps/web. A container's own build_context and dockerfile_path are read relative to the manifest's directory, and apply only while the app-level value is still the default. See Build a monorepo app in CI.

Validate and render

Registration and sync are covered in Set up an application and the dashboard's manifest page. To see what the server renders from the registered manifest, from CI or a shell:

export ASTROLIFT_API_URL="https://<your-control-plane>"   # the URL astro status prints
export ASTROLIFT_DEPLOY_TOKEN="<deploy token>"            # from the app's Tokens page
export ASTROLIFT_APP_SLUG="orders-api"                    # the app slug
astro ci render

Success prints the rendered Kubernetes resources as JSON and exits 0. Exit 2 with ASTROLIFT_API_URL, ASTROLIFT_DEPLOY_TOKEN, ASTROLIFT_APP_SLUG named is a missing variable. Exit 1 with manifest render error at <key path>: ... names the manifest key to fix; app "orders-api" not found or has no manifest to render means the slug is wrong or the app was registered without a manifest.

The target control plane is authoritative because provider capabilities and organization defaults participate in normalization and rendering. TOML syntax validation alone cannot prove a deployment is portable to a selected cluster.