> ## Documentation Index
> Fetch the complete documentation index at: https://docs.surfacearea.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Put data in a world

> Load rows into a world from a file, seed a live session, or pull a real run's calls, transform them with your own code or a declared mapping, and ingest them. One gate, the world's contract, on every route.

Rows go into a world three ways: into the world itself (a new version), into one live
session (no version), or out of a real run and back in as rows. All three use the same rows
file. The world's contract decides what is accepted.

If an agent is doing the work, `gateway worlds skill world-data-ingestion` prints the
agent-form checklist.

<img src="https://mintcdn.com/surface-d3d890e1/I9MKHQA4beHtjYQ2/screenshots/world-data-tab.png?fit=max&auto=format&n=I9MKHQA4beHtjYQ2&q=85&s=5fd073445aa19d0618e689cd0751e804" alt="A world's Data tab: entity rows and the batches that produced them" width="2880" height="1800" data-path="screenshots/world-data-tab.png" />

## The rows file

Pick one of three shapes per file.

**An object of entity to rows.** Several entities in one file.

```json theme={null}
{
  "users": [{ "id": "U1", "name": "Ada", "email": "ada@example.com" }],
  "channels": [{ "id": "C1", "name": "general", "created_by": "U1" }]
}
```

**JSON Lines, one row per line, each naming its entity.**

```jsonl theme={null}
{"entity": "users", "row": {"id": "U1", "name": "Ada"}}
{"entity": "channels", "row": {"id": "C1", "name": "general", "created_by": "U1"}}
```

**Bare rows with `--entity`.** A JSON array or JSON Lines of plain objects, for one entity.

```bash theme={null}
gateway worlds data import ./vendor-world users.jsonl --entity users
```

Entity names are the world's own: the entities in `schema/world.json`. Fields keep the
vendor's casing.

## Append or replace

* **`--mode append`** (the default) adds rows. A row whose primary key already exists
  updates that row rather than duplicating it.
* **`--mode replace`** removes every row of each entity the file names, then inserts the
  file's rows. Entities the file does not name are untouched.

The whole file is checked first. One bad row and nothing is written; the refusal points at
the row and the field, such as `/users/3/email`.

## 1. See what the world holds

```bash theme={null}
gateway worlds data show ./vendor-world
gateway worlds data show vendor-world --entity users     # a platform world, one entity's rows
```

The first line of the output is the world's kind. An import lands as a contract-checked batch
in `data/initial.json`.

### Ask for the rows you mean

`data show --entity` dumps an entity. `data query` filters it, pages it, and finds a redacted
row from the plaintext you know:

```bash theme={null}
gateway worlds describe vendor-world                                   # what is there, and how each field is stored
gateway worlds data query vendor-world users --where plan=pro --limit 50
gateway worlds data query vendor-world users --where email=jo@acme.test --resolve
gateway worlds data query vendor-world users --where id=1 --where id=2   # a repeated key is IN
```

`--where k=v` is equality on the stored value, repeatable; the same key twice means "either".
`--limit` and `--offset` page. Rows go to stdout as one JSON object per line, so the output
pipes into `jq`; the `n of total` footer and the `resolved` lines go to stderr. Without `--where`,
`data query` is `data show --entity` with a page size.

A world built from a vendor hashes its identifying fields on disk, so
`--where email=jo@acme.test` alone matches nothing. With `--resolve`, each value you pass is
turned into the form the store holds, per the world's `[redact]` policy, and the query runs on
that. A field the policy drops resolves to nothing, and the output says so, because that field
was never written. `worlds describe` shows which fields resolve, and to what.

The same query is `GET /api/public/worlds/{slug}/data?entity=&where=&resolve=true`, `queryRows`
in [the TypeScript SDK](/sdk-ts/describe), and the `query_world_data` tool for an agent. A world
whose rows live in a data snapshot answers it too, without a session.

### Ask in SQL

`data sql` runs one read-only SQL statement over a schema world's rows, whatever database the
world uses. Use it for joins, counts and `GROUP BY` that `data query` cannot express:

```bash theme={null}
gateway worlds data sql acme-world 'SELECT id, name FROM users LIMIT 5'
gateway worlds data sql ./acme-world 'SELECT plan, COUNT(*) AS n FROM users GROUP BY plan' --json
```

One statement only, and only reads: a write, `ATTACH`, `PRAGMA` or a second statement is
refused, and so is a statement that runs past 10 s, makes a value over 4 MiB or answers more
than 16 MiB. A blob comes back as `{"base64": "..."}`, and a float too large for JSON as the
text `"inf"` or `"-inf"`. At most 1000 rows come back (`--limit`); the output says when there were more. The
same read is `POST /api/public/worlds/{slug}/data/sql` with `{"sql": "..."}` and the
`sql_world_data` tool for an agent. A world built on `db/schema.sql` is a plain SQLite file:
read it with `sqlite3`.

## 2. Import

```bash theme={null}
gateway worlds data import ./vendor-world rows.json
gateway worlds data import vendor-world rows.json --message "seed Q3 tenants"
gateway worlds data import vendor-world rows.json --publish later
```

Give a local directory and `data/initial.json` is updated in place.

Give a platform world's slug and the rows stream up as gzipped chunks into a **data batch**.
The platform validates every row against the contract, applies the batch onto the world's
snapshot, and turns it into a data-only version. `--publish now` (the default) cuts that
version per batch; `--publish later` leaves the batch pending until you fold several together
with `data publish`.

Imported rows stay with the world as you keep building it. Add a tool, change a handler, a
route, a test or the docs, and the next version serves the same rows with no new import, from a
sandbox or a local checkout alike, and a fork of the world (`gateway bench fork`) starts with the
same rows. A change to the data's shape (an entity, a field or its description, a key or a
relationship) needs the rows imported again against the new shape, and `schema compile` names
what changed.

Exit 0 means applied or published, exit 2 means refused with a report written, exit 1 is an
error.

### Check before you write

```bash theme={null}
gateway worlds data check vendor-world rows.json --report refusals.jsonl
```

`data check` is `data import --dry-run` under its own name: every row is validated against
the world's contract and nothing is uploaded or written. A slug runs a dry-run batch on the
platform; a directory runs the import over a scratch copy. Exit 2 when any row is refused,
and `--report` writes every refusal as JSON Lines rather than only the first few.

### Redact on the way in

Imported rows are stored exactly as given. Pass `--redact` when they are not already redacted:

```bash theme={null}
gateway worlds data import vendor-world rows.json --redact apply
gateway worlds data check vendor-world rows.json --redact refuse
```

`apply` runs the world's `[redact]` policy over every row first; `refuse` rejects a row that
carries plaintext in a redacted field. Both need a `connector.toml`. See
[Keep real data out](/worlds/redaction).

<Info>
  Over MCP the same import is `import_world_data` with `slug` or `containerId`, `rows` in
  the object shape, and optional `mode`, `redact` and `message`. It answers `versionId` and
  `entityCounts`.
</Info>

## Large files: the batch lane

Large files go up as chunks, and `data import` against a slug does the chunking for you.

```bash theme={null}
gateway worlds data import vendor-world big.jsonl --chunk-bytes 32MiB --parallel 8
gateway worlds data import vendor-world big.jsonl --resume        # pick up where it stopped
```

| Flag | What it controls |
| - | - |
| `--chunk-rows` / `--chunk-bytes` | Rows, or pre-gzip JSON Lines bytes, per chunk. Default 64 MiB |
| `--parallel` | Chunk uploads in flight. Default 4 |
| `--resume` | Pick up the newest incomplete upload of this world and these files |
| `--atomic` / `--per-chunk` | One refusal refuses the whole batch (the default), or only its own chunk |
| `--merge upsert` / `--merge fill` | Inside the batch, by primary key: last wins, or first wins and later rows add fields |
| `--publish now` / `--publish later` | A version per batch, or fold with `data publish` |
| `--wait` / `--no-wait` | Wait for the batch to apply (the default), or print `{batchId, state}` once queued |
| `--report` | Write every refusal as JSON Lines |
| `--dry-run` | Validate against the contract and upload nothing |

### Watch a batch

```bash theme={null}
gateway worlds data status <batchId>          # state, progress, counts, first refusals
gateway worlds data batches vendor-world      # the world's batches, newest first
gateway worlds data publish vendor-world -m "Q3 tenants"
```

A batch moves through `open`, `uploading`, `queued`, `validating`, `applying`, and then rests
at `applied`, `refused`, `published` or `failed`. The last three of those are final, so
re-running an import against one is a no-op. `data publish` folds every applied
`--publish later` batch into a single data-only version.

The CLI records each upload under `.gateway/imports/`, which is what `--resume` reads and
what lets `data status` find a batch without being told its world.

## 3. Seed a live session

A session is one running copy of a version. Seeding changes that copy only, so it is where
you try rows before they become a version.

```bash theme={null}
gateway worlds session open vendor-world -t default --surface api
gateway worlds session seed <sessionId> rows.json
gateway worlds session seed <sessionId> rows.json --mode replace     # swap the named entities
gateway worlds session seed <sessionId> rows.json --redact apply     # redact the rows first
```

A world's session seeds live. When the rows are right, import them so the next version carries
them. Over MCP: `seed_world_session`, which takes `redact` alongside `rows` and `mode`; over
HTTP: `POST /api/public/world-sessions/{sessionId}/calls` with
`{"kind": "seed", "rows": {...}, "mode": "append"}`, and `{"args": {"redact": "apply"}}` when
the rows need redacting on the way in.

A task can carry the same `redact` mode in its `seed` block, so every session the task opens
seeds the same way. See [Keep real data out](/worlds/redaction#redact-on-the-way-in).

## 4. Turn a real run into rows

```bash theme={null}
gateway worlds session export <sessionId> --out calls.jsonl
```

Each line is one call, oldest first, with exactly seven keys: `{args, completedAt, error, kind,
result, seq, tool}`, where `kind` is `call`, `grade`, `seed`, `reset` or `close`. Reads carry
rows in their `result`; writes carry the row the world should hold in their `args` and
`result`. The REST and MCP forms of the same record add two more fields, `state` and
`createdAt`.

### Pull, transform, ingest

A tool's response is not the entity's shape, and nothing is inferred. The route is three
commands, each re-runnable:

```bash theme={null}
gateway worlds data calls pull calls.jsonl --from traces --session <sessionId>   # 1. the raw calls, untouched
gateway worlds data ingest acme-crm calls.jsonl --transform shape.py --dry-run     # 2. your code shapes them; nothing written
gateway worlds data ingest acme-crm calls.jsonl --transform shape.py               #    then for real
gateway worlds data query acme-crm accounts --limit 5                              # 3. prove it
```

#### 1. Pull the raw calls

`calls pull` writes tool-call records, one JSON object per line, and prints what they hold:
records per tool, and every distinct result shape (an object's top-level keys, an array's
item keys) with its count. Nothing is shaped.

```bash theme={null}
gateway worlds data calls pull calls.jsonl --from traces --trace <traceId> --trace <traceId>
gateway worlds data calls pull calls.jsonl --from traces --since 2026-09-01T00:00:00Z --tool crm_read
gateway worlds data calls pull calls.jsonl --from session --session <worldSessionId>   # a world session's call log
gateway worlds data calls pull calls.jsonl saved.json --from clickhouse-result         # a saved query result
```

```text theme={null}
wrote 13 records to calls.jsonl
13 records, 5 tools
  crm_log_activity: 2 records
    result {ok} (2)
  crm_read: 5 records, 1 failed
    result {contacts, deals, id, name, owner, tier} (2)
    result {account, account_id, amount, contact_id, id, last_activity_days, name, owner, stage} (1)
    result {account_id, email, id, name} (1)
    result null (1)
  ...
```

The record is `{tool, args, result, at, source}`, with `source` set to
`trace:<traceId>/<observationId>` for a trace and `session:<id>/<seq>` for a world session;
an `ERROR`-level observation carries `error` instead of `result`, and ingest skips it,
counted. `--from traces` reads `GET /api/public/observations?type=TOOL` (per `--trace`,
paged; `--since` and `--until` bound the window) or `GET /api/public/sessions/{id}?includeIO=true`.
A saved ClickHouse result is `{rows: [{id, trace_id, name, input, output, start_time, level}]}`
(text before the JSON is skipped); `input` and `output` are decoded as JSON, or kept whole
under `_raw`. `gateway worlds session export` lines and records written by `captureToolCalls`
in [the TypeScript SDK](/sdk-ts/ingest) (`capture_tool_calls` in Python) are the same record.

#### 2. Write the transform

A transform is your code. It exports `transform(records)` and returns `{entity: [rows]}` — a
`.py` file, or `.ts` / `.mjs` for node — or it reads the records as JSON Lines on stdin and
prints that document. It decides everything: which tool's result is which entity, what a
nested list becomes, what a write's `args` become, which call wins. The script runs in a
subprocess with a time limit (`--timeout`, default 120 s), sockets disabled and no `GATEWAY_*`
variables; its stderr is shown.

The CRM case, from the pull above: `crm_read`, `crm_search` and the two updates return CRM
objects (an account carries `tier` and nests `contacts` and `deals`; a deal carries `stage`
and nests its `account`; a contact carries `email`). `crm_log_activity` and
`crm_delete_contact` return nothing worth a row, so their rows come from the call's `args`,
keyed by the observation id. The latest call wins for an id.

```python filename="shape.py" theme={null}
ACCOUNT = ("id", "name", "owner", "tier")
CONTACT = ("id", "account_id", "name", "email")
DEAL = ("id", "account_id", "contact_id", "name", "stage", "amount", "owner", "last_activity_days")


def transform(records):
    rows = {"accounts": {}, "contacts": {}, "deals": {}, "activities": {}}

    def put(entity, obj, keys):
        if isinstance(obj, dict) and "id" in obj:
            rows[entity][obj["id"]] = {k: obj[k] for k in keys if obj.get(k) is not None}

    def absorb(obj):
        if isinstance(obj, list):
            for item in obj:
                absorb(item)
        elif isinstance(obj, dict):
            if "tier" in obj:
                put("accounts", obj, ACCOUNT)
                absorb(obj.get("contacts", []))
                absorb(obj.get("deals", []))
            elif "stage" in obj:
                put("deals", obj, DEAL)
                absorb(obj.get("account"))
            elif "email" in obj:
                put("contacts", obj, CONTACT)

    for rec in sorted(records, key=lambda r: r.get("at") or ""):
        if rec.get("error"):
            continue
        tool, args, at = rec["tool"], rec.get("args") or {}, rec.get("at")
        observation = (rec.get("source") or "").rsplit("/", 1)[-1]
        if tool in ("crm_read", "crm_search", "crm_update_deal", "crm_update_account"):
            absorb(rec.get("result"))
        elif tool == "crm_log_activity":
            row = {"id": observation, "account_id": args.get("account_id"), "deal_id": args.get("deal_id"),
                   "kind": args.get("kind") or "note", "note": args.get("note", ""), "at": at}
            rows["activities"][observation] = {k: v for k, v in row.items() if v is not None}
        elif tool == "crm_delete_contact":
            rows["activities"][observation] = {"id": observation, "kind": "contact_deleted",
                                               "note": f"deleted contact {args.get('id')}", "at": at}
    return {entity: list(held.values()) for entity, held in rows.items()}
```

```ts filename="shape.ts" theme={null}
type Rec = Record<string, unknown>;
const ACCOUNT = ["id", "name", "owner", "tier"];
const CONTACT = ["id", "account_id", "name", "email"];
const DEAL = ["id", "account_id", "contact_id", "name", "stage", "amount", "owner", "last_activity_days"];

export function transform(records: Rec[]): Record<string, Rec[]> {
  const rows: Record<string, Map<string, Rec>> = { accounts: new Map(), contacts: new Map(), deals: new Map(), activities: new Map() };
  const put = (entity: string, o: Rec, keys: string[]) =>
    void rows[entity]!.set(String(o.id), Object.fromEntries(keys.filter((k) => o[k] != null).map((k) => [k, o[k]])));
  const absorb = (obj: unknown): void => {
    if (Array.isArray(obj)) return obj.forEach(absorb);
    if (!obj || typeof obj !== "object") return;
    const o = obj as Rec;
    if ("tier" in o) { put("accounts", o, ACCOUNT); absorb(o.contacts ?? []); absorb(o.deals ?? []); }
    else if ("stage" in o) { put("deals", o, DEAL); absorb(o.account); }
    else if ("email" in o) put("contacts", o, CONTACT);
  };
  for (const rec of [...records].sort((a, b) => String(a.at ?? "").localeCompare(String(b.at ?? "")))) {
    if (rec.error) continue;
    const tool = String(rec.tool), args = (rec.args ?? {}) as Rec, obs = String(rec.source ?? "").split("/").pop() ?? "";
    if (["crm_read", "crm_search", "crm_update_deal", "crm_update_account"].includes(tool)) absorb(rec.result);
    else if (tool === "crm_log_activity") {
      const row: Rec = { id: obs, account_id: args.account_id, deal_id: args.deal_id, kind: args.kind || "note", note: args.note ?? "", at: rec.at };
      rows.activities!.set(obs, Object.fromEntries(Object.entries(row).filter(([, v]) => v != null)));
    } else if (tool === "crm_delete_contact") rows.activities!.set(obs, { id: obs, kind: "contact_deleted", note: `deleted contact ${String(args.id)}`, at: rec.at });
  }
  return Object.fromEntries(Object.entries(rows).map(([entity, held]) => [entity, [...held.values()]]));
}
```

#### 3. Ingest

```bash theme={null}
gateway worlds data ingest acme-crm calls.jsonl --transform shape.ts --dry-run --report refused.jsonl
gateway worlds data ingest acme-crm calls.jsonl --transform shape.ts -m "from session 8817"
```

The rows then go through exactly what `data import` does: the contract, refusals naming
entity, index and field, primary-key upsert, the batch lane for a slug. The report is `N
records, N ingested, 0 skipped, R refused — via transform`, rows per entity, the refusals as
`[index] pointer: reason`, and for a slug the batch and its version; `--json` prints it as one
document. Exit 0; 2 when a row was refused; 1 when the transform failed (its stderr is on
yours) or the platform did.

<Info>
  Redaction is optional and off by default: rows land as your transform returned them. Pass
  `--redact apply` to run the world's `[redact]` policy over them first, or `--redact refuse`
  to reject plaintext in a redacted field. See [Keep real data out](/worlds/redaction).
</Info>

### Ingest from any source: a declared mapping

`ingest.toml` declares where every field comes from, with no code. A field with no line is
never set; a record no `[[sources]]` claims is reported by name; nothing is inferred.

```toml filename="ingest.toml" theme={null}
[ingest]
on_conflict = "latest"      # two rows at one primary key: the later $at wins | first
unmatched = "refuse"        # a record no [[sources]] claims: refuse | skip (counted)

[[sources]]
name = "crm_read"                   # a tool name, a list of them, or "*" for rows and csv records
select = "result"                   # path into the record: result | result.data[*] | args | $record
when = { tier = { present = true } }   # optional: every condition must hold on the selected item
entity = "accounts"
[sources.fields]                    # entity field <- item path, or a table
id = "id"
name = { from = "name", transform = ["trim"] }
tier = { from = "tier", default = "smb" }
[[sources.explode]]                 # a nested list, or one object -> rows of a child entity
path = "contacts"
entity = "contacts"
carry = { account_id = "id" }       # child field <- parent entity field
[sources.explode.fields]
id = "id"
email = { from = "email", transform = ["lower"] }

[[sources]]
name = "crm_log_activity"
select = "args"                     # a write's row is its arguments
entity = "activities"
[sources.fields]
id = "$source.id"                   # the record: $source.id, $source.trace, $tool, $at, $index, $session, $record.<path>
kind = { from = "kind", default = "note" }
note = "note"
at = "$at"

[[sources]]
name = "crm_delete_contact"
select = "args"
entity = "activities"
[sources.fields]
id = "$source.id"
kind = { literal = "contact_deleted" }
note = { template = "deleted contact {id}" }
at = "$at"
```

| A field is | Meaning |
| - | - |
| `"path"` or `{ from = "path" }` | the value at the path; `emails[0]` indexes, `$parent.id` reaches the parent item inside an explode |
| `transform = [...]` | a chain from the closed set: `lower`, `upper`, `trim`, `collapse_ws`, `nfkc`, `digits`, `email_local_part`, `email_domain`, `sha1`, `sha256`, `strip_prefix:<p>` |
| `default = ...` | used when `from` is missing or null; without one the field is not set |
| `type = "string" \| "integer" \| "number" \| "boolean"` | coerces; a CSV cell is text until this says otherwise, and a cell that cannot be coerced refuses the record |
| `{ literal = ... }` | a constant |
| `{ template = "deleted contact {id}" }` | `{path}` holes filled from the item or the record; an empty hole refuses the record |
| `{ hash = ["field", "$tool"] }` | the sha256 of those values (a JSON array), for an id the source does not carry; unset when one is missing |

The file is checked when it loads: every entity and field must exist in the contract, every
transform must be in the set, every `carry` must name a field the parent maps. `[*]` walks a
list in `select` and `explode.path`; `select = "result[*]"` reads an array result item by
item. `at = "updated_at"` on a source dates its rows from the item for `on_conflict`;
otherwise the record's `at` does.

```bash theme={null}
gateway worlds data ingest acme-crm calls.jsonl --map ingest.toml --dry-run
gateway worlds data ingest acme-crm saved.json --from rows --map ingest.toml      # JSON objects; a saved result is one record, select rows[*]
gateway worlds data ingest acme-crm deals.csv --from csv --map ingest.toml        # header row; every cell is text
gateway worlds data ingest acme-crm calls.jsonl                                   # the world's own ingest.toml, when it has one
```

The dry run reports per source: records seen, items selected, rows per entity, **unmapped
source fields** with a sample value, and **entity fields never set**; then the unmatched
records by name and every refusal with its record index.

```text theme={null}
13 records, 10 ingested, 1 skipped, 0 refused (dry run) — via map
  accounts: 3 rows
  activities: 3 rows
  contacts: 3 rows
  deals: 2 rows
  skipped: call failed 1
  unmatched (no [[sources]] claimed them, skipped): crm_read 1
  source crm_read, crm_update_account (tool-call, select result) -> accounts: 2 records, 2 items, rows accounts 2, contacts 1, deals 1
    unmapped source fields: error ("not found")
  source crm_delete_contact (tool-call, select args) -> activities: 1 records, 1 items, rows activities 1
    activities fields never set: account_id, deal_id
  entity counts: accounts 3, activities 3, contacts 3, deals 2
```

`map init` writes the file to start from: one `[[sources]]` per source shape in a sample
(per tool for tool calls), every source field with a sample value on one side, every entity
field on the other. Only an exact-name match is filled in, and each one is marked `# exact-name
match — confirm`; every other field is a commented `# entity.field = ""` line.

```bash theme={null}
gateway worlds data ingest map init ./acme-crm calls.jsonl                 # writes ./acme-crm/ingest.toml
gateway worlds data ingest map init acme-crm saved.json --from rows --entity accounts --out -
```

### Tool calls that already are the operation's response

With neither a transform nor a mapping, and no `ingest.toml` in the world, a tool-call record
goes through its tool's `[[operations]]` entry in `connector.toml`: the `result` is read at the
operation's `results` path and projected by its `[ingest]` table (`drop`, `explode`,
`carry_args` from the call's `args`) exactly as [a capture](/worlds/custom-connections) is.
Use it when a tool's result is the vendor's response body and nothing else. The report says
`via operations` and notes that this is the operation's projection, not a mapping; a tool that
is not one of the world's operations is refused by name.

```bash theme={null}
gateway worlds data ingest ./vendor-world calls.jsonl
gateway worlds data ingest vendor-world --from traces --session <sessionId> --redact apply
```

### Rows already shaped

`--from rows` with nothing declared is `data import`: a rows file, or bare rows with
`--entity`, through the same gate. A file the world already fits needs no route at all.

### Let `data extract` do the shaping

`data extract` reads a tool-results export, a raw vendor response or a session export and
writes rows already shaped for `data import`:

```bash theme={null}
gateway worlds data extract vendor-world calls.jsonl \
  --shape session-export --entity messages --out messages.jsonl
```

It drops null and empty fields, strips fields the entity does not declare (and counts them),
dedupes by primary key — `--merge fill` keeps the first row, `--merge upsert` the last — and
runs the world's `[redact]` policy over every row unless you pass `--no-redact`. The summary
on stderr is `{"read": N, "rows": M, "unknownFields": {...}, "redacted": {"dropped": n, "hashed": n}}`.

<Info>
  A real run holds real records. `data extract` redacts by default; `data import` and
  `data ingest` do not, so pass `--redact apply` when rows from a run go straight in. See
  [Keep real data out](/worlds/redaction).
</Info>

## Captures are the other way in

For a vendor you can call, `gateway worlds connector capture` records what the real service
returns and `gateway worlds connector ingest` seeds the world from it through the same
contract. See [Mock any vendor API](/worlds/custom-connections).

## Where to go next

* [Keep real data out](/worlds/redaction) for the `[redact]` policy and the `--redact` modes.
* [Getting started](/worlds/getting-started) for the whole path from an empty directory to a graded run.
* [Mock any vendor API](/worlds/custom-connections) for captures, handlers and conformance.
* [MCP Server](/mcp) for `import_world_data`, `seed_world_session` and `export_world_session`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.