# Worlds and seeding

Named worlds, the shared design studio, seeding your own records with POST /_rystic/seed, the registers that hold them, rystic describe, and the seed gate.

A simulator boots empty unless you give it a starting state: a named world built into the binary, a seed of your own, or both. The examples use the Lob simulator; every simulator takes the same calls.

## Named worlds

```bash
rystic pull lob-twin
PORT=$(rystic run lob-twin -d --name worlds --port 0 --scenario design_studio --json | jq -r .data.port)
curl -s localhost:$PORT/_rystic/scenarios | jq -c '.scenarios[] | {name, frames, loop}'
# -> {"name":"design_studio","frames":0,"loop":null}
```

1. Without `--scenario` (or `RYSTIC_SCENARIO`), a simulator boots empty: `GET /_rystic/inspect` answers `{}`, or only the records the service always holds, such as Okta's Everyone group.
2. `POST /_rystic/scenario` resets, then loads a world, with no restart. A world loaded this way holds exactly the state the same world booted with `--scenario` holds.

```bash
curl -s -X POST localhost:$PORT/_rystic/scenario -d '{"name":"design_studio"}'
# answers 204
```

An unknown name answers `404`, naming the worlds the build has:

```json
{"message":"unknown scenario \"design-studio\" (have: design_studio)"}
```

### List the worlds

| Ask | How |
|---|---|
| A running simulator's worlds | `curl -s localhost:$PORT/_rystic/scenarios` |
| The same, without starting one | `rystic run lob-twin --name menu --port 0 -- --list-scenarios` |
| What each world holds | `rystic docs lob-twin scenarios` prints the installed `SCENARIOS.md`; Stripe ships none |
| Worlds, registers and operations at once | `rystic describe lob-twin` ([below](#rystic-describe)) |

[Discovering routes and scenarios](https://www.rystic.ai/docs/control-plane.md#discovering-routes-and-scenarios) covers the container image and the bare binary.

### The shared studio

`design_studio` is one company, Website Design Contractors, on Lob, Okta, PayPal, Resend, Slack and Stripe. [Environments](https://www.rystic.ai/docs/environments.md) runs them together.

### A world's timeline

A world can carry events that fire as its clock passes them: Lob, Okta, PayPal and Resend's `design_studio` do. The clock stays put until you move it ([Pacing](https://www.rystic.ai/docs/concepts/reset-and-determinism.md#pacing)):

```bash
curl -s -X POST localhost:$PORT/_rystic/clock -d '{"advance_seconds":3600}'
curl -s localhost:$PORT/_rystic/inject | jq -c '.events[0] | {id, fired_at, status}'
# -> {"id":"harborview-second-office","fired_at":"2026-03-01T01:00:00Z","status":200}
```

[Timelines](https://www.rystic.ai/docs/control-plane.md#timelines) has the event format.

## Seeding

`POST /_rystic/seed` lands records directly, without calling the API. It answers `204`.

```bash
curl -s -X POST localhost:$PORT/_rystic/seed -d '{"state_address": {"adr_home": {"id": "adr_home",
  "object": "address", "name": "HOME", "address_line1": "185 BERRY ST",
  "address_city": "SAN FRANCISCO", "address_state": "CA", "address_zip": "94107"}}}'
curl -s -u test_x: localhost:$PORT/addresses/adr_home | jq -c '{id, name}'
# -> {"id":"adr_home","name":"HOME"}
```

### Two shapes

1. **Register map**: `{"<register>": {"<key>": {<record>}}}`, as above.
2. **World document**: the register map under `seed`, beside optional `clock`, `rng_seed`, `network`, `credentials` and `events`. Other keys are ignored, so expected outcomes can live in the same file. [Boot inputs](https://www.rystic.ai/docs/control-plane.md#boot-inputs) describes each key.

```bash
cat > world.json <<'EOF'
{
  "seed": {"state_address": {"adr_home": {"id": "adr_home", "object": "address", "name": "HOME",
    "address_line1": "185 BERRY ST", "address_city": "SAN FRANCISCO", "address_state": "CA", "address_zip": "94107"}}},
  "clock": "2026-07-23T00:00:00Z",
  "expect": {"postcards": 1}
}
EOF
curl -s -X POST localhost:$PORT/_rystic/reset
curl -s -X POST localhost:$PORT/_rystic/seed --data-binary @world.json
# answers 204; the clock now reads 2026-07-23
```

Either shape also boots: `rystic run lob-twin --seed world.json`, or `RYSTIC_SEED` for a container.

### How a seed lands

1. A seed merges into what the simulator holds; it does not reset first. A new key adds a record.
2. An existing key merges field by field: `{"adr_home": {"name": "HOME 2"}}` changes the name and keeps the address. A nested object, such as `metadata`, replaces the old one whole.
3. `--scenario` with `--seed` applies the world first, then merges the file on top.
4. `GET /_rystic/inspect` returns the state as a register map, so a dump posted back after a reset restores it.

### What a seed refuses

A refusal answers `400` with `{"message", "errors"}`, and nothing lands. Only these are refused:

1. An unknown register. The error names every register the simulator has:

   ```json
   {"errors":[{"register":"state_adress","error":"unknown register \"state_adress\"; this twin has: state_address, state_idempotency_key, state_postcard"}],"message":"seed rejected: 1 invalid entries"}
   ```

2. A record that is not a JSON object: `record "<key>" must be a JSON object`.
3. A world document carrying a `tape` or `loop`. Those play only from a built-in scenario.
4. An incomplete record, under `RYSTIC_SEED_GATE=reject` ([below](#the-seed-gate)).

## Registers

A register holds one kind of record. `GET /_rystic/registers` lists each one: `id`, `kind`, `key_fields`, `seed_via` and its `fields`, each with its `type`, and `required` where the model requires it.

```bash
curl -s localhost:$PORT/_rystic/registers | jq -c '.registers[] | {id, kind, key_fields}'
# -> {"id":"state_address","kind":"document_collection","key_fields":["id"]}
# -> {"id":"state_idempotency_key","kind":"idempotency_store","key_fields":["idempotency_key"]}
# -> {"id":"state_postcard","kind":"document_collection","key_fields":["id"]}
```

A register with an `owner_field` files each record under one caller, as Kalshi's portfolio registers do ([Kalshi → Seeding state](https://www.rystic.ai/docs/simulators/kalshi.md#seeding-state)).

## rystic describe

`rystic describe <product>` asks the installed binary what it serves, with no simulator running.

```bash
rystic describe lob-twin
# -> scenarios (1) — `rystic run lob-twin --scenario <name>`; …
# ->   design_studio  Website Design Contractors' account: the Portland, ME offi…
# -> registers (3) — seed a keyed one with POST /_rystic/seed {<id>: {<key>: <record>}}:
# ->   state_address          document_collection (key id, 17 fields)
# ->   …
# -> operations (17): DELETE 4, GET 8, POST 5
```

1. `-json` adds every register's typed fields and an example record. Post `{"<register>": <example>}` to `/_rystic/seed` as it is.
2. `-v` lists every operation.
3. With no product named, it describes every installed simulator. [CLI flag reference → describe](https://www.rystic.ai/docs/cli-reference.md#describe).

## The seed gate

A record missing a field its register marks `required` is still accepted by default, and served as though whole. The simulator's log says so. Okta's password register requires `salt` and `verifier`:

```bash
rystic pull okta-twin
OKTA=$(rystic run okta-twin -d --name gate --port 0 --json | jq -r .data.port)
curl -s -X POST localhost:$OKTA/_rystic/seed -d '{"state_user_password": {"00u1": {"userId": "00u1"}}}'
# answers 204
rystic logs gate | grep "incomplete seed"
# -> rystic: WARNING — incomplete seed: state_user_password record "00u1" is missing 2 required field(s): salt, verifier. …
rystic stop gate
```

1. `RYSTIC_SEED_GATE=reject` refuses such a seed instead: `400`, `seed rejected: 1 incomplete record(s)`, naming each missing field.
2. `RYSTIC_SEED_GATE=off` skips the check.
3. The check runs on the merged record, so a partial update to a complete record passes.
4. Set it at boot: `rystic run okta-twin --env RYSTIC_SEED_GATE=reject`, or in the container's environment.

Done with the Lob example: `rystic stop worlds`.

---

Source: https://www.rystic.ai/docs/concepts/worlds · Markdown: https://www.rystic.ai/docs/concepts/worlds.md
