How AGNT5's control plane separates projects from environments, and why that split keeps deploys boring.
A surprising number of decisions in a platform turn out to rest on one question: what is the unit of deployment? Once you pick, a lot of other things — routing, secrets, billing, access control — have to agree with the answer. Pick badly, and users push back in a hundred small ways. Pick well, and the interface disappears.
We picked project as the unit of deployment, and environment as the lifecycle target inside it. The split shows up everywhere in the control plane, and once you see it, the rest of the platform’s shape makes sense.
What a project is
A project is the smallest thing you can independently version, deploy, and bill for. Roughly: a git repo, a team’s ownership, a set of workers that share code. One project usually corresponds to one application — a support agent, a document-indexing pipeline, a research assistant. The project owns its code bundle, its worker specs, its environment variables, its secrets.
Projects belong to tenants. A tenant is the customer account boundary; a project is the application boundary inside it. Most tenants have between three and thirty projects. The bulk email summarizer and the onboarding agent are separate projects, even if they share a team.
What an environment is
An environment is a named target within a project. The defaults are dev, staging, and prod. Teams often add preview for ephemeral PR environments or canary for a slice of production traffic.
Each environment has its own:
- Deployment — a running version of the project’s code.
- Runtime endpoint — a URL like
myproject-prod.run.agnt5.com. - Worker pool — isolated from other environments’ pools.
- Secrets and variables — configured independently.
- Run history — scoped to runs that executed against that environment.
Environments within a project share the same code bundle format, the same registered component schema, and the same billing rollup. They do not share workers. A run dispatched to prod will not be picked up by a worker that registered against staging.
What the CLI and SDK see
The split shows up in the URL. A subdomain like myproject-prod.run.agnt5.com encodes the project and environment. The SDK client reaches a specific environment by hitting the right subdomain:
from agnt5 import Client
# Dev
dev = Client("https://myproject-dev.run.agnt5.com")
# Production
prod = Client("https://myproject-prod.run.agnt5.com")
# Or use an API key against the top-level gateway and pass scoping explicitly.
api = Client(gateway_url="https://api.agnt5.com", api_key=os.environ["AGNT5_API_KEY"])
result = api.run(
"research_agent",
input={"topic": "durable execution"},
project="myproject",
environment="prod",
)Deploys go through the CLI. agnt5 deploy --env=staging builds an image, pushes it to the project’s registry, and rolls out a deployment into the staging environment. The dev-plane operator picks up the deployment, spins up the worker replicas, and registers them with the coordinator under the staging scope.
Why two levels and not one
We could have collapsed this into a single concept — just deployments, with no project grouping. Many platforms do. The trouble is that most things that are interesting to say about a running application are said at the project level, not the deployment level.
Billing aggregates at the project. Secrets are usually configured once per project and varied per environment. Access control (“who can deploy?”) is typically granted on the project, not on each environment individually. A run list for “my support agent” groups across environments by default — users want to see their app, not a specific deployment of it.
Environments, meanwhile, are the thing that changes on a daily rhythm. You push to dev, you promote to staging, you canary into prod. The lifecycle verbs — deploy, rollback, promote — all act on an environment. Projects are stable. Environments move.
Keeping the two concepts separate means the stable thing (project) owns the stable data, and the moving thing (environment) owns the mutable data. A rollback in staging does not affect prod. A secret rotated in prod does not leak into dev. A new environment — a preview env for a PR — inherits the project’s settings and overrides what it needs to.
What lives in the control plane vs the runtime
The control plane (the Go services) owns the project and environment metadata, the deployment state, and the access-control policies. When you call the CLI to deploy, the request goes to the control plane’s API, which persists the deployment record, interacts with the Kubernetes operator, and rolls out workers.
The runtime (the Rust data plane) does not know about projects as a concept — it knows about tenants. Projects resolve to tenant-scoped prefixes before reaching the runtime. A run created against project myproject in environment prod ends up with a tenant-scoped segment on disk, tagged with metadata that lets the gateway and query layer surface it correctly.
This separation is why the runtime stays small. The control plane is the place to reason about “who can deploy to which environment,” and the runtime is the place to reason about “how do we execute this run reliably.” Trying to teach either one the other’s concerns makes both of them worse.
Environments as a default, not a constraint
One thing we deliberately avoided: making environments first-class in a way that forces three of them on every project. A solo developer shipping a side project does not need dev/staging/prod. They need one environment — call it prod or main, whatever — and an ability to blow it away and recreate it.
The control plane exposes environment creation as an API, not a fixed enum. A project can have one environment or fifteen. Preview environments for PR branches are created on demand by a CI hook and destroyed when the PR closes. The cost of extra environments is a bit of metadata and a worker pool; nothing deeper.
Why this matters
The project-environment model is not a feature users ask for by name. It is the scaffolding that makes every other feature — secrets, deploys, observability, access control — feel consistent. “Where does this configuration live?” always has a clear answer. “What does a rollback affect?” always has a clear scope. “Which runs am I looking at?” always resolves to a precise filter.
The payoff is that the developer experience stays calm. Deploys are boring. Promotions are boring. Rollbacks are boring. That is the shape we wanted.