Environments · Checkpoints · A/B Tests · Canary Rollouts · Promotion Workflow · Targeting Segments
AgentStack lets you fork project data into isolated environments (sandboxes, staging, A/B variants). Each environment has a generation number: production is generation=0; each fork increments generation. Sandboxes store diffs (changed fields only) on top of a parent entity; full state is resolved by merging the ancestor chain. That keeps forks cheap and makes checkpoints and rollbacks predictable.
Related: OPENAPI.md (Swagger, schema, /api-docs) · ACCESS_AND_FIELD_POLICY.md (API keys, roles, and Field Access Policy) · Subscription limits below.
- Core Concepts
- Architecture
- Subscription Limits
- REST API Reference
- Flow Diagrams
- Merge resolution & caching
- A/B Tests
- Canary Rollout
- Promotion Workflow
- Targeting Segments
- Shadow Writes
- Protected Field Security
- Dashboard UI (product)
- Code Examples
- FAQ
| Concept | Description |
|---|---|
| Environment | An isolated context with its own generation number. Production is always generation=0. |
| Generation | Version layer of an entity. Each fork increases generation by 1. |
| parent_uuid | UUID pointer to the parent entity from which a diff was created. Forms the lineage chain. |
| Diff entity | A sandbox entity that stores only changed fields (not the full snapshot). |
| Checkpoint | A fully materialised merge of the entire ancestor chain — a snapshot frozen in time. |
| Environment anchor | The project-side record that owns environment metadata (name, status, pointers to head/pinned UUIDs). |
| Head pointer | data.config.head_uuid — always points to the latest diff in the environment. |
| Pinned pointer | data.config.pinned_uuid — immutable snapshot UUID used for share / review links. |
draft → validating → ready → rolling_out → live → archived
| State | Meaning |
|---|---|
draft |
Just created, not yet validated |
validating |
Pre-promotion checks running |
ready |
Passed all checks, ready to promote |
rolling_out |
Canary traffic ramp-up in progress |
live |
Fully promoted to production |
archived |
Inactive; TTL cleanup eligible |
Environment state is stored in project JSON (8DNA) — anchors, diffs, and related metadata live alongside your normal project data.
Project records ← environment anchors and forked project state
Audit / backup area ← promotion requests, A/B metadata (implementation detail)
Per-user copies ← optional forked user rows for segment-based tests
| Field | Type | Description |
|---|---|---|
env_type |
"production"|"sandbox"|"staging"|"ab_test"|"feature" |
Environment type |
env_name |
string |
Human-readable label |
is_checkpoint |
bool |
True = materialised full-merge snapshot |
diff_only |
bool |
True = entity stores only changed fields |
status |
state machine value | See state machine above |
head_uuid |
UUID |
Pointer to latest diff |
pinned_uuid |
UUID |
Immutable review snapshot |
traffic_weight |
0–100 |
Canary traffic percentage |
rollout_steps |
array |
Stepwise canary config |
ab_seed |
string |
Stable bucketing seed for A/B split |
segments |
array |
Reusable targeting segments |
shadow_writes |
bool |
Mirror production writes into this env |
expires_at |
ISO datetime | TTL — env auto-archived after this |
Production entity (generation=0, parent_uuid=null)
↓ fork
Sandbox diff entity (generation=1, parent_uuid=<prod_uuid>)
↓ accumulates changes
Checkpoint (generation=2, is_checkpoint=true — full merged data)
The platform walks the parent_uuid chain and deep-merges JSON from root to the target UUID (so a sandbox row only stores overrides). Merged results are cached briefly on the server to keep repeated reads fast; cache is invalidated when the chain changes.
| Limit | Free | Starter | Basic | Pro | Premium | Enterprise |
|---|---|---|---|---|---|---|
| Environments | 0 | 1 | 3 | 10 | 30 | Unlimited |
| Generation depth | 0 | 3 | 5 | 10 | 20 | Unlimited |
| Checkpoints | 0 | 1 | 3 | 10 | 30 | Unlimited |
| A/B tests | 0 | 0 | 1 | 5 | 20 | Unlimited |
| Shadow writes | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ |
| Env TTL (days) | 0 | 7 | 14 | 30 | 90 | No expiry |
Free plan: Backup and rollback of production entities only. No sandbox environments.
Starter plan: 1 sandbox environment, 3 generations deep. Suitable for solo developers testing a feature branch.
All endpoints are under /api/sandbox. Auth: project API key or user JWT in Authorization: Bearer <token>.
| Method | Path | Description | Auth |
|---|---|---|---|
POST |
/api/sandbox/fork |
Fork production project into a new environment | 🔑 JWT |
GET |
/api/sandbox/environments |
List all environments for a project | 🔑 |
DELETE |
/api/sandbox/environment |
Archive and cleanup an environment | 🔑 JWT |
{
"source_project_id": 42,
"env_name": "feature/payments-v2",
"env_type": "sandbox",
"user_filter": null,
"segment_id": null
}Response:
{
"anchor_uuid": "...",
"fork_project_uuid": "...",
"env_name": "feature/payments-v2",
"env_type": "sandbox",
"generation": 1,
"forked_count": 3,
"manifest_uuid": "...",
"status": "draft"
}| Method | Path | Description |
|---|---|---|
POST |
/api/sandbox/checkpoint |
Materialise a full-merge snapshot |
POST |
/api/sandbox/rollback |
Branch a new environment from a checkpoint |
{
"env_uuid": "<anchor_uuid>",
"label": "before-payment-migration"
}| Method | Path | Description |
|---|---|---|
POST |
/api/sandbox/promote |
Initiate promotion (creates a promotion request record) |
POST |
/api/sandbox/approve |
Approve a pending promotion request (Enterprise) |
{
"env_uuid": "<anchor_uuid>",
"strategy": "immediate"
}Strategies: immediate, canary, blue_green.
Pre-promotion checks run automatically:
| Check | What it validates |
|---|---|
schema_valid |
Ancestor chain merges without error |
depth_ok |
Ancestor chain ≤ 10 generations |
conflict_free |
No other environment currently in rolling_out state |
health_ok |
Env status is ready, validating, or live |
If any check fails, promotion is blocked and the response contains checks with per-check booleans.
| Method | Path | Description |
|---|---|---|
POST |
/api/sandbox/canary/advance |
Advance to next rollout step |
POST |
/api/sandbox/canary/abort |
Abort canary — rollback to previous traffic weight |
{ "env_uuid": "<anchor_uuid>" }| Method | Path | Description |
|---|---|---|
POST |
/api/sandbox/pin |
Create a pinned immutable snapshot UUID |
GET |
/api/sandbox/tree |
Return full generation tree for visualisation |
GET |
/api/sandbox/diff |
JSON diff between two entity states |
POST |
/api/sandbox/protected-check |
Server-side hash-check for a protected field |
GET /api/sandbox/diff?uuid_a=<prod_uuid>&uuid_b=<env_uuid>&project_id=42&table=<entity_kind>
Use the table value required by your deployment (see Swagger UI or OPENAPI.md — it identifies which root entity type to compare).
Response:
{
"added": { "config.new_feature": true },
"removed": {},
"modified": { "config.payment_provider": { "old": "stripe", "new": "tochka" } }
}| Method | Path | Description |
|---|---|---|
POST |
/api/sandbox/ab-test |
Create a new A/B test |
GET |
/api/sandbox/ab-test/{test_id}/metrics |
Get live split metrics |
GET |
/api/sandbox/ab-test/{test_id}/variant |
Get variant assignment for a user |
POST |
/api/sandbox/ab-test/{test_id}/result |
Record a result event |
POST |
/api/sandbox/ab-test/{test_id}/promote |
Declare winner and promote |
{
"env_a_uuid": "<control_uuid>",
"env_b_uuid": "<variant_uuid>",
"split_pct": 50,
"metric": "conversion_rate"
}{
"test_id": "...",
"env_a": { "requests": 1200, "conversions": 84, "rate": 7.0 },
"env_b": { "requests": 1195, "conversions": 107, "rate": 8.95 },
"winner": null,
"status": "active"
}| Method | Path | Description |
|---|---|---|
POST |
/api/sandbox/segment |
Create a reusable targeting segment |
GET |
/api/sandbox/segments |
List all segments for a project |
{
"name": "premium-users",
"rules": [
{ "field": "subscription_tier", "op": "eq", "value": "premium" },
{ "field": "country", "op": "in", "value": ["US", "DE", "GB"] }
]
}1. VALIDATE — subscription limits
2. ANCHOR — create environment anchor (draft status)
3. PROJECT — fork project row as diff-child (generation=N+1)
4. USERS — fork filtered users (segment or user_filter)
5. SUB-ENTITIES— fork any additional tracked entities
6. MANIFEST — audit record for the operation
7. CACHE — invalidate merge cache for the chain
All steps run inside a single database transaction. Failure at any step triggers a full rollback.
1. Resolve full merged state (ancestor chain)
2. Insert new row: is_checkpoint=true, diff_only=false, full data
3. Update anchor's head_uuid to checkpoint UUID
4. Invalidate merge cache for the chain
1. Run pre-promotion checks (schema, depth, conflict, health)
2. Create promotion request record (audit trail)
3. If strategy=canary → status=rolling_out, traffic_weight=first_step
4. If strategy=immediate → status=live, swap prod pointer
5. Enterprise: require approval via /api/sandbox/approve
6. On approve → swap generation pointers in production
For any UUID in a lineage, the platform reconstructs effective JSON by walking parent_uuid and deep-merging from root to leaf. Checkpoints materialise that merged state as a full snapshot row.
Caching: merge results are cached server-side with a short TTL; any write to the chain clears the relevant cache so you do not see stale merged state.
Integration surface: use REST (/api/sandbox/*, tree, diff, promote) and the published API schema (OPENAPI.md · Swagger). Server-side SDKs or internal modules are not part of the public contract.
A/B tests use stable bucketing (hash-based assignment): hash(user_uuid + ab_seed) % 100. The same user always gets the same variant across restarts and horizontal scaling.
Use the REST routes under /api/sandbox/ab-test/* (create test, metrics, variant, result, promote). See Swagger UI (tag Sandbox) or OPENAPI.md for request/response shapes.
Canary rollout shifts production traffic gradually via traffic_weight (0–100). Steps are defined in rollout_steps on the anchor's config.
[
{ "weight": 5, "pause_minutes": 30 },
{ "weight": 20, "pause_minutes": 60 },
{ "weight": 50, "pause_minutes": 120 },
{ "weight": 100, "pause_minutes": 0 }
]Advance or abort via POST /api/sandbox/canary/advance and POST /api/sandbox/canary/abort (see Swagger for bodies).
Each promotion is recorded as an audit object (strategy, checks, approvers, timestamps). Example shape:
{
"env_uuid": "...",
"strategy": "canary",
"status": "pending_approval",
"checks": {
"schema_valid": true,
"depth_ok": true,
"conflict_free": true,
"health_ok": true
},
"approvers": [],
"approved_at": null,
"created_at": "2026-03-25T12:00:00Z"
}Enterprise: promotion may require explicit POST /api/sandbox/approve before traffic is switched.
Segments are stored as rules in project.data.config.segments[] and evaluated when forking or routing.
| Operator | Meaning |
|---|---|
eq |
Field equals value |
neq |
Field does not equal value |
in |
Field is in list |
not_in |
Field is not in list |
gt / lt |
Greater / Less than (numeric) |
contains |
Field string contains value |
POST /api/sandbox/fork
Content-Type: application/json
{
"source_project_id": 42,
"env_name": "premium-test",
"env_type": "sandbox",
"segment_id": "<segment_uuid>"
}Only users matching the segment rules are forked into the sandbox.
When data.config.shadow_writes=true on an environment anchor, production writes to tracked entities can be mirrored asynchronously into the sandbox as diff-patches. The production response is not delayed — mirroring is fire-and-forget.
Available from Pro plan (see Subscription Limits).
POST /api/sandbox/fork{
"source_project_id": 42,
"env_name": "staging",
"env_type": "staging"
}If your stack supports shadow writes, the platform can mirror production writes into a staging environment as diff patches asynchronously (production latency is not increased). Wire this through your platform integration or SDK if available; otherwise rely on REST and deployment-specific docs.
Sandbox environments do not expose decrypted secrets. Requests with ?decrypt_protected=true in a sandbox context are rejected (403 sandbox_secret_access_denied).
Use server-side hash verification instead:
POST /api/sandbox/protected-check
Content-Type: application/json
{
"env_uuid": "<anchor_uuid>",
"field_path": "keys.api.payment_secret",
"value": "sk_test_my_key"
}{ "match": true }The plaintext is compared on the server; the response only indicates match or no match.
The hosted web app includes a sandbox area with: environment switcher, generation tree, diff viewer, A/B dashboard, canary controls, promotion flow, and segment builder. Exact layout and labels may change between releases — use REST and OpenAPI (e.g. Swagger) as the stable integration surface.
Use fetch or your HTTP client with Authorization: Bearer <token> and the endpoints in this guide. Example: list environments, then fork — see Swagger UI or openapi.json for full schemas (OPENAPI.md).
If you use the official web dashboard, it may expose client hooks or helpers; those are not part of the public API contract.
Sandbox clients should send:
X-AgentStack-Env: feature/payments-v2The platform resolves this header so downstream requests run in the correct environment (generation and type are applied server-side).
Alternatively, store the active env in the user session:
{ "data": { "sandbox": { "active_env": "<env_uuid_or_name>" } } }Q: Does forking copy all production data?
A: Fork creates lightweight diff-children. Full state is resolved on demand by merging the ancestor chain, so only changed fields are stored in the sandbox.
Q: Are protected fields (API keys, secrets) copied to sandboxes?
A: No. Protected fields are not copied on fork; resolution reads them from the appropriate ancestor. In sandbox context, ?decrypt_protected=true is blocked — use POST /api/sandbox/protected-check.
Q: How is A/B user assignment stable?
A: Stable bucketing with hash(user_uuid + ab_seed) % 100. The ab_seed is set at test creation and stays fixed.
Q: What happens when an environment exceeds its TTL?
A: Periodic cleanup archives expired environments (status=archived) and cleans up diff-children. Production (generation=0) is never removed.
Q: What does canary traffic_weight control?
A: It is stored on the environment anchor. Your routing layer (gateway, edge config, or app code) should read the published traffic weight for that environment to decide what share of traffic uses the new version.