An environment is the thing you reason about and evaluate against. A
clone is the component it composes.
ENVIRONMENT (named, version-controlled via environment.yaml)
└── members: clone ids ← the single source of truth for membership
└── CLONE isolated SaaS-app instance
├── backend per-clone database + Redis prefix
└── frontend optional (full mode, Slack only)
Two nouns at two altitudes: the clone is the unit you build and compose; the
environment is the unit you point an agent at and score. Membership lives on the
environment (Environment.members), never on the clone — one source of truth, so
the two can’t drift.
Shape
| Field | Example | What it is |
|---|
name | acme | The handle you pass to env commands. |
members | ["slack-a1b2", "stripe-c3d4"] | Clone ids in this environment. The source of truth. |
spec | acme.env.yaml or null | Path to the environment.yaml that produced it, or null for ad-hoc (spin -g) environments. |
state | running | Rolled up from members: running (all up), partial (some up), stopped, failed. |
createdAt / updatedAt | ISO | Timestamps. |
Two ways to create one
Declaratively, from a version-controlled spec. List as many clones as you
want — env spin brings them up in declared order:
# acme.env.yaml
name: acme
clones:
- template: slack
mode: full
seed: acme-corp
- template: stripe
mode: api
asymmetric env spin acme.env.yaml
Ad-hoc, by tagging clones as you spin them — each clone joins the named
environment:
asymmetric spin slack stripe --group acme # both clones land in environment "acme"
Either way, operate the environment as a unit:
asymmetric env status # list environments and their members
asymmetric env reset acme # reset every member to its clean seeded state
asymmetric env destroy acme # tear down every member
See the env command reference for the full surface.
How env spin behaves
The spin is sequential and continue-all: each clone is created and joined to
the environment before the next is started, and one member’s failure doesn’t
abort the rest.
- Every
create() self-persists, so a crash mid-spin still leaves a real
environment holding the members spun so far.
- A failed member is rolled back (never recorded), and the environment is stamped
partial; the command exits non-zero.
The schema is strict
The environment.yaml schema rejects anything it can’t actually honor,
loudly, rather than parsing it and silently doing nothing:
| In the spec | Today |
|---|
One or more clones entries | Spun, in order. |
identity: (shared users/org across clones) | Hard error — not supported yet. |
data: (bring-your-own Postgres) | Hard error — not supported yet. |
| An unknown key | Hard error, with a “did you mean” hint. |
Declared-but-unbuilt features fail loudly on purpose. For an eval product, a
silently-ignored identity: block would make results quietly wrong. A clear error
is the honest behavior until the feature is real.
On the roadmap
A shared cross-clone identity graph (one set of users/org spanning every
member) and bring-your-own Postgres data are reserved in the schema but not
yet enforced. The data model and the environment.yaml schema already make room
for them, so nothing about today’s shape has to change when they ship.
The registry
Environments and clones are both persisted to ~/.asymmetric/registry.json
(schema-versioned, migrated on read). See
Clones, environments, and the registry.