> ## 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.

# gateway worlds

> Every gateway worlds subcommand, grouped by the workflow it belongs to — author a contract, put data in, publish a version, open live sessions, run an agent, and test the world in CI.

`gateway worlds` covers a [world](/worlds) end to end: its contract, its rows, its versions, the live sessions an agent calls, and the tests it ships. The groups below follow that order.

Every command reads credentials the way [the CLI overview](/cli) describes, and `--host` overrides the host on any of them.

<Info>
  Run `gateway worlds <group> --help`. The help text is generated from the
  command tree, so it matches the build you have installed.
</Info>

## Author a world

A world starts from a shipped connector template, a connector your organization published, or a blank contract you write yourself.

| Command | What it does |
| - | - |
| `worlds schema templates` | List the connector templates the package installs; `custom` is the blank contract. `--json` adds each template's entities and tools. |
| `worlds schema init <path>` | Create a world directory from a template and compile it. `--connector <slug>` picks the template (`custom`, a shipped slug such as `salesforce`, or `workspace:<slug>` for one of your own); `--name` sets the world name. A shipped template scaffolds offline with the bundled runtime (a `python3` 3.12 or newer): no host, no keys. Only `workspace:<slug>` reads the connector from the platform and needs a host; without the runtime a shipped template is scaffolded on the platform too. |
| `worlds create [slug]` | Create the world on the platform in one call, from a template, a `workspace:<slug>` connector, or a directory. |
| `worlds list` | The project's worlds with their latest version and linked scenario set. `--slug` filters, `--limit`/`-n` caps the list. |

```bash theme={null}
gateway worlds schema templates
gateway worlds schema init ./vendor-world --connector custom --name vendor-world
```

Use `worlds create` when the contract already exists. It is the same request as `POST /api/public/worlds` and the MCP tool `create_world`, and it answers with the world plus its first `READY` version.

```bash theme={null}
gateway worlds create acme-crm --from workspace:acme --rows seed.json -m "first cut"
```

It prints one line ending in `url`, the world's page in the app (`https://<host>/project/<projectId>/world?env=<containerId>`). `--json` prints the whole answer, whose `links` carry `api` (the world's JSON descriptor, `https://<host>/world/<slug>`), `world` (the same descriptor at `/api/public/worlds/<slug>`), `tasks` and `sessions`.

Its flags: `--from` (template slug, `workspace:<slug>`, or a directory), `--overlay` (files laid over the source before compiling), `--rows` (repeatable seed file) with `--entity` for bare-row files, `--name`, `--description`, `--message`/`-m`, and `--json`. `--batch <file>` reads JSONL lines of `{slug, from, overlay?, data?, name?}` and prints one result line each, exiting `2` when any line failed.

### The world is stored as its schema tree

The platform stores the `gateway-world/1` tree as you wrote it, with one session serving both its tools and its HTTP routes.

## Shape the contract

The contract in `schema/world.json` declares entities, fields, keys and relationships. `connector.toml` declares the vendor connection and the routes the world answers. Two commands edit them without hand-editing the files.

| Command | What it does |
| - | - |
| `worlds schema entity add <path> <entity>` | Declare one entity. Exactly one of `--ref` (an existing `$defs` entry), `--fields id:string,name:string,archived:boolean?`, or `--schema` (inline JSON Schema). `--key`/`-k` sets the primary key (repeat for composite), `--unique` and `--relationship field->target` add the rest. |
| `worlds schema entity remove <path> <entity>` | Remove one entity from `schema/world.json`, with the `$defs` only it used. Refused (exit `2`, nothing written) while a `connector.toml` operation answers from it, a `verifiers/*.sql` reads it, `data/initial.json` holds rows of it, a `[dependencies.<alias>.entities]` map names it, or another entity relates to it; each reference is named on its own line. The world keeps at least one entity, so add the replacement first. An untouched placeholder `verifiers/state_present.sql` is re-pointed at an entity that stays. |
| `worlds connector route add <path> <name>` | Append one `[[operations]]` entry. `--method`/`-X`, `--path`/`-p` (with `{arg}` segments), `--param`, `--entity`, `--results` (the row array in a captured page), `--envelope` (the key the served answer wraps its rows under; `""` for a bare array), `--action` (`read`, `create`, `update`, `delete`, `handler`), `--status`, `--single`, `--filter arg=row_field`, `--body row_field=request.body.path`, `--handler`. `--replace` rewrites the block already declared under that name instead of refusing it. The first route creates `connector.toml` with `[auth] scheme = "bearer"` and `secret_env = "<SLUG>_API_KEY"`. `--action handler --handler <fn>` also writes a stub `def <fn>(ctx, args, query, body):` answering `501 {"error": "not_implemented", …}` into `api/handlers.py`, unless the file already defines it; the JSON output's `handler` is `{file, function, written}`. |

```bash theme={null}
gateway worlds schema entity add ./vendor-world accounts \
  --fields id:string,name:string,archived:boolean? --key id

gateway worlds connector route add ./vendor-world list_accounts \
  -X GET -p /v1/accounts --entity accounts --results data
```

The `custom` scaffold ships one example entity, `records`, and `schema init` lists `gateway worlds schema entity remove <dir> records` among its next steps. Declare your own entities first, then remove the example:

```bash theme={null}
gateway worlds schema entity remove ./vendor-world records
```

A new `connector.toml` authenticates with a bearer token, the kind a hosted session hands the agent. `gateway worlds serve` answers 401 to a request without one and accepts the bearer `--api-key` pins (`conform` when you pass none).

The entity must exist before a route can name it. Declare write routes too: a lookup-only mock tests nothing about what an agent does when it creates, updates and polls.

### Check what you wrote

| Command | What it does |
| - | - |
| `worlds schema compile <path>` | Compile the contract and write back what the compiler produced (lock, runtime, database files). |
| `worlds schema check <path>` | Check the contract and write nothing. Refuses a dotted `[redact]` path that can match nothing (`contacts.phone` is a path inside a row, not an entity scope). The report's `tools` is the declared handlers; `handler_execution.served` is every name a session answers (handlers plus connector routes and a dependency's `alias.tool`). On a stale tree it says whether `compile` will succeed or refuse because a journal holds rows written against the previous contract. |
| `worlds schema describe <path>` | Print the contract's entities, tools and vendor connection; a field with an enum shows its values (`one of: …`). `--json` for the document. |
| `worlds connector validate <path>` | Parse `connector.toml` and list its operations. |
| `worlds connector conform <path>` | Serve the world and check every operation's answer against the vendor's OpenAPI: the documented status, and a body the vendor's response schema accepts. `--openapi <file>` (repeatable), `--out` for the JSON report, `--limit` for the first N operations. |
| `worlds schema reconcile <path>` | Reconcile the contract with what the vendor really returned: relax required fields the captures omit, add undocumented keys as optional properties, drop formats the captured values do not meet. `--from` names where the observed rows come from (default `captures`); `--write` rewrites `schema/world.json` and recompiles. |
| `worlds schema ui init <path>` | Declare `[ui]` in `connector.toml` when it is missing and generate `ui/static` (index.html, app.js, contract.json) from the contract. `compile` regenerates it; `[ui] generated = false` keeps a hand-written one. See [Give a world a UI](/worlds/ui). |

```bash theme={null}
gateway worlds schema compile ./vendor-world
gateway worlds schema check ./vendor-world
gateway worlds connector conform ./vendor-world --openapi vendor.yaml --out conform.json
gateway worlds schema reconcile ./vendor-world --write
```

`conform` measures fidelity. Its failure list is what to fix to make the replica behave like the vendor.

`reconcile` works through that list. Vendor documents drift from live accounts: snake\_case where the docs say camelCase, undocumented keys, required fields the account omits, timestamps without a zone. Reconciling against `captures/` fixes those without hand-editing. It changes top-level fields only, prints every change it makes, and writes nothing until `--write`. It runs on a local `python3` 3.12 or newer.

## Capture data from the real vendor

Two connector commands turn live vendor responses into a world's starting data.

| Command | What it does |
| - | - |
| `worlds connector capture <path>` | Call the live connection once, following its cursor, and record every page under `captures/`. `--op` names the operation, `--arg name=value` and `--param name=value` supply arguments, `--max-pages` overrides the configured cap. |
| `worlds connector ingest <path>` | Seed `data/initial.json` from the successful captures, gated by the contract. `--batch-id` names the batch, `--dry-run` checks every row and writes nothing, `--report <file>` writes the refusals as JSONL. |

`--remote` runs the capture on the platform instead — from the pod whose address the vendor allow-listed, using the credential held as a project secret — and pulls the redacted pages back into `captures/` here. `--environment` names the world for that path and `--timeout` bounds the wait.

```bash theme={null}
gateway secret set VENDOR_TOKEN
gateway worlds connector capture ./vendor-world --op list_accounts --remote
gateway worlds connector ingest ./vendor-world --dry-run --report refused.jsonl
gateway worlds connector ingest ./vendor-world
```

An ingest stops at the first row the contract refuses. `--dry-run` reports every refusal at once, grouped by capture, entity and pointer, and exits `2` when there is one. `--report` needs `--dry-run`, and writes the same records `data import --report` does plus the capture each came from.

<Info>
  A capture makes real calls against a real account. Scope the credential, name
  the one operation you want, and cap the pages before running it.
</Info>

## Put rows into a world

`worlds data` is the way in for rows that do not come from a capture. A rows file is `{"<entity>": [row, ...]}` JSON, `{entity, row}` JSONL, or bare rows with `--entity` naming what they are.

| Command | What it does |
| - | - |
| `worlds describe <world>` | What the world holds and how each field is stored: entities, keys, relationships, row counts, and the `[redact]` treatment per field (`[hash]`, `[preserve]`, `[round:n]`, `[drop]`). A field with an enum shows its values, e.g. `status: string  one of: requested \| approved \| rejected`, and carries `enum` in `--json`. A directory or a platform slug; `--json` for the document. |
| `worlds data show <world>` | Every entity's row count at the head, or one entity's rows with `--entity`. |
| `worlds data query <world> <entity>` | The rows you mean: `--where k=v` (repeatable; a repeated key is IN), `--limit`, `--offset`, `--json`. `--resolve` turns each value into the form the store holds under the world's `[redact]` policy, so a hashed field is found from its plaintext; a dropped field resolves to nothing, and the output says so. `--across` asks every linked world too, applying each mapping's transforms; `--compare` adds the parity table (platform slugs only). |
| `worlds data import <world> <rows...>` | Put rows in through the world's own gate. |
| `worlds data check <world> <rows...>` | Dry run: every row checked, nothing uploaded and nothing written. Exit `2` when any row is refused. Takes `--entity`, `--mode`, `--merge`, `--redact` and `--report` as the import would. |
| `worlds data extract <world> <source>` | Rows out of a tool-results export, a vendor response, or a session export, shaped for `data import`. `--shape tool-results\|vendor-envelope\|session-export`, `--entity`, `--out`, `--merge`, `--no-redact`. |
| `worlds data calls pull <out> [inputs...]` | Raw tool-call records out, untouched, as `{tool, args, result, at, source}` JSONL, and a summary of what they hold: records per tool and every distinct result shape with its count. `--from traces` (`--trace` repeatable, `--session`, `--since`, `--until`), `--from session --session <worldSessionId>` (a world session's call log), or `--from clickhouse-result` with the saved files as inputs. `--tool` keeps named tools, `--json` prints the summary as a document, `-` writes the records to stdout. |
| `worlds data ingest <world> [inputs...]` | Records into rows through the route you declare, then the world's gate. `--transform <script.py\|.ts\|.mjs>` runs your `transform(records) -> {entity: [rows]}` (or a stdin-to-stdout script) in a subprocess with `--timeout` (120 s), sockets disabled and its stderr shown; `--map <ingest.toml>` applies a declared mapping (the world's own `ingest.toml` when neither is given); with neither, a tool-call record goes through its `[[operations]]` projection in `connector.toml`, and the report says so. `--from tool-calls` (default; a `session export` works as is), `traces` (`--trace`, `--session`, `--since`), `rows` (JSON objects; with nothing declared, a plain `data import`), or `csv` (header row). `--tool`, `--mode`, `--redact` (optional, off by default), `--dry-run`, `--report`, `--json`, `-m`. Exit `2` when any row is refused, `1` when the transform failed. |
| `worlds data ingest map init <world> <sample...>` | Write an `ingest.toml` skeleton from a sample: every source field with a sample value on one side, every entity field on the other; only exact-name matches filled in, each marked to confirm. `--from`, `--entity`, `--out` (`-` for stdout; default `ingest.toml` in a world directory). |
| `worlds data status <batchId>` | One batch's state, progress, counts and first refusals; `--report <file>` writes every refusal as JSONL. `--world` when no manifest names it. |
| `worlds data batches <world>` | The world's data batches, newest first; `--limit` caps them. |
| `worlds data publish <world>` | Fold every applied `--publish later` batch into one data-only version. `--message`/`-m` records the reason. |

```bash theme={null}
gateway worlds data check acme-crm rows.jsonl --entity accounts --report refused.jsonl
gateway worlds data import acme-crm rows.jsonl --entity accounts -m "September accounts"
gateway worlds data show acme-crm
gateway worlds data query acme-crm accounts --where owner_email=jo@acme.test --resolve
gateway worlds data calls pull calls.jsonl --from traces --session <sessionId>
gateway worlds data ingest acme-crm calls.jsonl --transform shape.py --dry-run
gateway worlds data ingest acme-crm calls.jsonl --transform shape.py -m "from session 8817"
gateway worlds data ingest map init ./acme-crm calls.jsonl
gateway worlds data ingest acme-crm calls.jsonl --map ingest.toml --dry-run
gateway worlds data ingest acme-crm --from traces --session <sessionId> --redact apply   # the operations projection
```

The `<world>` argument decides where the rows land. A directory writes into the tree. A slug streams the rows up as gzipped chunks into a data batch that the platform validates, applies onto the world's snapshot, and turns into a version.

`import` flags worth knowing: `--mode append|replace` (append upserts by primary key), `--merge upsert|fill` inside the batch, `--redact off|apply|refuse` (default `off`), `--publish now|later`, `--atomic` (one refusal refuses the batch, the default) against `--per-chunk`, `--wait`/`--no-wait`, `--resume` to pick up an interrupted upload, `--chunk-rows`/`--chunk-bytes`/`--parallel` for large files, `--report` for the refusals as JSONL, and `--dry-run`.

`--redact off|apply|refuse` decides what happens to plaintext on the way in, and `data import`, `data check`, `data ingest` and `session seed` all take it. Redaction is optional: `off` is the default and rows land as given. [Keep real data out](/worlds/redaction) covers `apply` and `refuse`.

<Info>
  A refused row names the field that broke the contract and leaves the world
  unchanged. `--report refused.jsonl` works on `import` and `check` alike.
</Info>

Full detail, including million-row uploads, lives on [Put data in a world](/worlds/data).

## Publish a connector template

`worlds connector publish <path>` saves the template as a workspace connector — created on the first publish, a new commit after — so other projects can start worlds from the same contract.

| Command | What it does |
| - | - |
| `worlds connector publish <path>` | Publish the template. `--slug`, `--message`/`-m`, `--description`, `--tag` (repeatable), `--workspace-visible`, `--project-scope`. |
| `worlds connector list` | Your organization's workspace connectors. `--hub` adds the ones other organizations published; `--query` filters by slug, name or description. |
| `worlds connector pull <slug>` | Write a connector's template files to a directory. `--out`, `--version` (a label or commit hash; the head by default). |
| `worlds connector clone <source>` | Copy a connector another organization published into yours, with provenance. `--name`, `--slug`, `--project-scope`. |

A world directory is mapped back into the template layout on publish; `data/`, `captures/`, `db/` and generated files stay behind. The published connector equals the directory, so a path you removed is removed there too.

Publishing the *world* — the versioned bundle a session runs — is `gateway bench push`. See [bench & versions](/cli/bench).

## Open live sessions

A session is one live copy of the world, pinned to a version, opened on a task. The agent calls it over the surfaces it asked for.

| Command | What it does |
| - | - |
| `worlds session open <slug>` | Open a session and print where it answers. `--surface api\|ui\|browser` (repeatable or comma-separated); `ui` needs a declared `[ui]`, `browser` needs `ui`. `--browser-tier standard\|premium`: browser capacity for a World Host session, standard (spot, may be reclaimed) or premium (on-demand); default: the organization's setting. |
| `worlds session list` | The project's sessions in every state, from provisioning to stopped, newest first; the live ones are `--state RUNNING` and `--state IDLE`. `--state`, `--slug`, `--limit`, `--json`. |
| `worlds session status <id>` | The session's state, tools, surfaces, `idleExpiresAt` (null while pinned) and `pinned`; warns on stderr when under five minutes of idle time are left. |
| `worlds session state <id>` | Dump every entity's rows as the live world holds them now. |
| `worlds session seed <id> <rows...>` | Put rows into the live world and wait for the engine's answer. `--entity`, `--mode append\|replace`, `--redact off\|apply\|refuse` ([what the modes mean](/worlds/redaction)). |
| `worlds session grade <id>` | Run the session's task grader against the current state; prints `{reward, rewards, raw}`. `--timeout` overrides the grader's budget. |
| `worlds session reset <id>` | Put the world back to its opening state — base data plus the task's seed. The call log stays. |
| `worlds session export <id>` | The call log as JSONL, oldest first, one `{seq, kind, tool, args, result, error, completedAt}` per line. `--out` writes a file. `--requests` appends the world's own call log from the live session (one `state` call) as `kind: "request"` lines: every call the world answered, through the browser, straight to `api.url` or a tool by name, with `via` and credential fields `[redacted]` ([details](/worlds/sessions#the-rest-of-the-session-commands)). |
| `worlds session browse <id>` | Open the session's UI in its browser surface and print what the agent would read. `--mode hosted\|local\|auto`, `--path`, `--screenshot <file>`. |
| `worlds session pin <id>` | Pin the session so it never closes for being idle; `--off` unpins. |
| `worlds session close <id>` | End the session. `--keep-warm` hands the container back to the warm pool. |
| `worlds session reopen <id>` | Bring a closed session back, same id, from its last checkpoint, and print what `status` prints. Refused (exit `1`) while it still runs (`session_not_closed`), when it was never checkpointed (`session_no_checkpoint`), while another reopen is under way (`session_reopen_in_progress`, retry), or when its world version is gone (`session_version_unavailable`). |
| `worlds session task save <id>` | Promote a session's inline task into a stored task. `--name`/`-n`, `--world`/`-w`, `--no-world`, `--message`/`-m`. |

```bash theme={null}
gateway worlds session open acme-crm --task refund-double-charge --surface api
gateway worlds session status <sessionId>
gateway worlds session grade <sessionId>
gateway worlds session close <sessionId>
```

`open` first prints `session <id> opening — gateway worlds session status <id>` on stderr, so a wait cut short keeps the id. It then prints `{sessionId, ready, surfaces, api.url, ui.entry, browser.wsUrl, task, idleExpiresAt, pinned}`. Ask for surfaces beyond `tools` with `--surface`/`-s` (`api`, `ui`, `browser`, repeatable or comma-separated); the platform answers `400` rather than quietly serving fewer than you asked for. Pin an exact version with `--version-id`, and pass `--no-wait` to return as soon as the platform accepts the session. Otherwise `open` waits for `ready` up to `--timeout` seconds (600 by default): a session that fails to open exits 1 with the reason and its code, and one still opening when the time runs out is printed with `ready: false` and exits 1 while it keeps opening.

<Info>
  A World Host session's `api` block also carries a `token`. Send it as
  `Authorization: Bearer <token>` in a header on every request to `api.url` —
  never in the URL.
</Info>

Three flags choose the task the session opens on, and the response's `task` block reports which was used.

| Flag | Opens on |
| - | - |
| `--task`, `-t` | One of the version's bundled tasks, by name. |
| `--task-id` | A stored task, at its current spec or the one named by `--task-version`. |
| `--task-file` | An inline spec — `{instruction, seed?, grader?, metadata?, name?}` — for a task the world never published. |

A session closes on its own after thirty idle minutes. Pinning keeps its URL up until you close it. A closed session can come back with `reopen`: its checkpoint is the platform's last export of it while it ran.

## Keep tasks as project resources

A stored task is an instruction, a seed and a grader kept by the project rather than baked into a bundle, so many sessions can open on it by id.

| Command | What it does |
| - | - |
| `worlds task create <spec>` | Store a task from a JSON spec file and print it at version 1; it appears on the project's Scenarios page (under "Stored tasks", naming its worlds and what grades it) and on the Scenarios tab of every world it brings up. `--name`/`-n`, `--world`/`-w` to pin and validate it against that world, `--message`/`-m`. Names are unique per project: a taken name is refused (409) unless `--replace`, which stores the spec as that task's next version instead. |
| `worlds task update <task_id>` | `--spec <file>` becomes the next version (its scenario on the project's Scenarios page is rewritten); `--name`, `--world`, `--no-world` change in place. |
| `worlds task list` | The project's stored tasks, most recently changed first. `--world`/`-w` lists every task that runs against that world — pinned to it, or naming it under an alias (`aliasesHere` says which); `--metadata key=value` (repeatable), `--page`, `--limit`. Each task carries `verifiers`: one row per check or verifier (`key`, the aliases it judges, what it checks). |
| `worlds task show <task_id>` | One task with its spec, its `verifiers` rows and version history; `--version` shows an older spec. |
| `worlds task validate <task_id>` | Check the seed and grader entities against a world version without opening a session. `--version`, `--world`/`-w`, `--version-id`. Exit `1` with the problems listed. |
| `worlds task delete <task_id>` | Remove the task and every version (its scenario is archived, not erased); open sessions keep their copy. |

```bash theme={null}
gateway worlds task create ./tasks/refund.json --world acme-crm -m "from the Sept incident"
gateway worlds task validate <taskId>
gateway worlds session open acme-crm --task-id <taskId>
```

`validate` catches a grader asserting on an entity the world does not define, which would otherwise pass against an empty result. For a task whose spec names its `worlds`, `validate` checks each alias against the ref it names and answers `checked: "worlds"` with a per-alias result.

### Tasks that need several worlds

A spec with a `worlds` table — `{alias: {slug, ref?, seed?, grader?, tools?}}` — is one instruction over several hosted worlds. These commands take a task id or the manifest `task up --out` wrote; see [Spin worlds up and down](/worlds/sessions#tasks-that-need-several-worlds).

A check that needs more than one world goes in the spec's `verifiers` list: each has a `name`, a `scope` of two or more aliases, and an `assertions` or `python` grader that reads rows as `<alias>.<entity>` and call logs as `<alias>.calls`. `task create`, `task update --spec` and `session open --task-file` in the npm CLI check the file before sending: an alias outside `worlds` or listed twice, a one-alias scope, a `rubric` kind, a repeated name, a repeated check id, a key that does not belong to the verifier's kind, a check entity without an alias from the scope, or a first scope alias whose grader is a `rubric` exits `2` with nothing sent. From the Python CLI, the platform catches the same mistakes and the command exits `1`. Without `worlds`, a verifier runs on the session of its first scope alias, so the spec's `name` must be that alias. A verifier's reward shows up in `task grade` as `x.<name>`; see [Verifiers that span worlds](/worlds/sessions#verifiers-that-span-worlds).

```json theme={null}
{
  "instruction": "Open a ticket for the refund, then post its number in chat.",
  "worlds": { "support": { "slug": "acme-support" }, "chat": { "slug": "acme-chat" } },
  "verifiers": [
    {
      "name": "used_both",
      "scope": ["support", "chat"],
      "kind": "assertions",
      "checks": [
        { "entity": "support.calls", "assert": "exists" },
        { "entity": "chat.calls", "assert": "exists" }
      ]
    }
  ]
}
```

| Command | What it does |
| - | - |
| `worlds task up <task_id\|spec.json>` | Bring every world up in one platform call and print one manifest keyed by alias. `--task-version`, `--surface`/`-s` (asked of every world), `--browser-tier standard\|premium` (every world's browser; default: the organization's setting), `--no-wait`, `--out <file>`. All or nothing: a world that fails closes the rest. |
| `worlds task status <task_id\|manifest>` | The manifest read back from the platform. With a task id, refuses when an alias is up twice. |
| `worlds task grade <task_id\|manifest>` | `{reward, rewards, worlds, ungraded, verifiers, errors}`: per-world grades, flattened rewards as `<alias>` and `<alias>.<check>`, the mean of the worlds that answered a number; a grader that could not grade is named under `errors`, the reward is `null` and it exits `1`. `--timeout`. |
| `worlds task export <task_id\|manifest>` | Every world's call log as JSONL with a `world` field on each line. `--out`. `--requests` adds each world's own request log from its live session, as `kind: "request"` lines. |
| `worlds task down <task_id\|manifest>` | Close every session. With a task id, every live session of the task. `--keep-warm`. |

```bash theme={null}
gateway worlds task up <taskId> --surface api --out orion.manifest.json
gateway worlds task grade orion.manifest.json
gateway worlds task down orion.manifest.json
```

### Run an agent against a multi-world task

`worlds task run` does `up`, one call to your agent, `grade`, filing and `down` in a single command — in either CLI:

```bash theme={null}
# Python (pip install gatewaysdk)
gateway worlds task run <taskId|spec.json> -a my_agent.py -m claude-sonnet-4-5 --out-dir runs

# TypeScript (npm install @withgateway/sdk)
gateway worlds task run <taskId|spec.json> --agent ./agent.mjs -m claude-sonnet-4-5 --out-dir runs
```

`--surface`/`-s` and `--browser-tier standard|premium` work as on `task up`. `--agent`/`-a` is `file.py[:fn]` or `module:fn` in Python (`fn` defaults to `agent`); in TypeScript it is a
`.js`/`.mjs`/`.cjs` module (or `.ts` with `tsx` installed next to it), `file.mjs[#export]` — the export defaults to
the module's default export (falling back to a named `agent` export), and `#name` picks a specific named export.
Both are called as `fn(task)` where `task` is a small `TaskRun` object: `.runId`/`.run_id`, `.instruction`, `.model`,
`.sessions` (alias -> `WorldSession`), `.manifest` (the same object `task up` prints), `.api(alias)` -> `(url,
token)` (a tuple in Python, `{url, token}` in TypeScript), `.toolkit()` (every world's tools merged into one, each
name prefixed `<alias>.<tool>` — a world with none contributes nothing), and `.record({...})` to log notes into the
run's `run.json`. The agent's return value is the report graders see: a string, or an object/dict with a `report`
key (other keys land in `run.json` as-is). A coroutine (Python) or a `Promise` (TypeScript) the agent returns is
awaited for you.

Every world is graded with that report (`WorldSession.grade`'s `report` argument, sent as `{"report": ...}` — a
rubric judge reads it as the agent's account of what it did), one rollout is filed the way `worlds run` files a
single-world run (`versionId` anchored to the task's first world; every world's own version travels in
`metadata.worlds`), and every session closes — all of this happens
even when the agent raises, with the error recorded in `run.json["agentError"]` and the filed evaluation marked
FAILED. `--no-file` skips filing, `--no-trace` skips tracing the agent's own session, `--keep-up` leaves the
sessions open instead of closing them, `--surface`/`-s` asks every world for more than `tools` (comma-separated or
repeated). The filed run is one line on the **Runs** tab of every scenario set that holds the task.

A run that fails is still recorded: when a world never comes up or a world's grade cannot be read, `run.json` says
why (`runError`, or `gradeError` naming the world), every other world's grade is kept, and the evaluation is filed
FAILED with that reason and no reward; a filing the platform refuses is `fileError` (exit 1). Ctrl-C or SIGTERM
closes every session the run opened (unless `--keep-up`), even while the platform is still opening them (the run
waits for its answer). Before the grade, it writes `run.json` with `runError: "interrupted by SIGTERM"` (or
`SIGINT`) and files nothing; after the grade, the filing in flight finishes first. Either way it exits 130 (Ctrl-C)
or 143 (SIGTERM).

The run writes `<out-dir>/<run id>/manifest.json`, `grades.json`, `run.json` and (when the agent gave one)
`report.md`, where `<run id>` is `wtr-<12 hex>`. The agent's own LLM calls and tool spans are traced under one
platform session named by the run id, with the host and keys the command uses (`--host`, `GATEWAY_*`, or the saved
`gateway auth login`); `run.json["traced"]` says whether tracing actually ran — it never fails the run. A
`GATEWAY_OTLP_ENDPOINT` on another host is followed only with the environment's own `GATEWAY_PUBLIC_KEY` /
`GATEWAY_SECRET_KEY`: a saved login's keys go to their own host only. When tracing cannot start, the run continues
untraced and the process prints one warning saying why (never a key); `--no-trace` runs untraced without it. The
TypeScript CLI traces through optional `@opentelemetry/*` packages that `npm install -g @withgateway/sdk` does not
install: the warning prints the `npm install -g` line that adds them. See
[Group traces into a session](/sdk-ts/sessions#group-a-task-runs-own-spans-into-one-session) for how the TypeScript
SDK does the grouping.

A world whose tools the platform could not read (its compiled tool contract is over the publication cap; the
TypeScript `session open` and `task up` print it as `warning (tool-contract-unread)`) is refused before the agent
starts: `task run` in both CLIs and the TypeScript `worlds run` print one line naming the world and the reason, exit 1,
close every session (even with `--keep-up`) and file nothing. `task run-set` refuses that task the same way, runs the
set's other tasks and closes the set run failed. The Python `worlds run` loads the world in-process and reads its tools
there.

### Run every task of a set with one model

`worlds task run-set` runs each stored task of a scenario set the way `task run` does, and files them all under one
**set run**: the set page's **Runs** tab then shows who ran it, from which branch, commit and pull request, with which
model, how many tasks are done, the score, and every task's checks.

```bash theme={null}
gateway worlds task run-set <set-id> --agent ./agent.mjs -m claude-sonnet-5
```

* One model per set run. Run it again with another `-m` to compare models side by side.
* The score is the mean of the tasks that were graded; a check or task with no score is shown as such, never as 0.
* The checkout is read from the working directory (branch, commit, remote without credentials). The pull request
  comes from `GATEWAY_PR_URL` (and `GATEWAY_PR_TITLE`), or from GitHub Actions' `GITHUB_REF` on a `pull_request` run.
* The run closes as `failed` (exit 1) when an agent raised or a task could not run or be graded; a low score is still `completed`.
* Ctrl-C or SIGTERM starts no further task: the task in flight closes its sessions, the run closes as `failed` naming it, and the command exits 130 or 143.
* From code: `runSet(setId, agent, { model })` (TypeScript) or `run_set(set_id, agent, model=...)` (Python, `from gatewaysdk import run_set`).

## Edit on the platform: the sandbox

`worlds sandbox` edits a stored world server-side: open a READY version, write and run there, check the tree against the push gate, commit a new version. Nothing lands on this disk. `bench sandbox` is the same set of verbs.

| Command | What it does |
| - | - |
| `worlds sandbox open <slug[@ref]>` | Open a sandbox on a READY version (source only; the data snapshot stays a pointer). Prints the sandbox id. |
| `worlds sandbox list` | The project's sandboxes, newest first — find one you left open. `--status open\|closed`, `--slug`, `--limit`, `--json`. |
| `worlds sandbox status <id>` | Base version, open/closed, last use, and when it closes if left idle (`idleExpiresAt`). |
| `worlds sandbox read <id> <path>` | Print a file from the sandbox tree, uncommitted edits included. |
| `worlds sandbox write <id> <path>` | Write one file, up to 256 MB, from `--from <file>`, `--content <text>` or stdin. `--from <dir>` writes every file under it except `.git/`, `.gateway/` and scratch such as `node_modules/`. |
| `worlds sandbox exec <id> '<command>'` | Run a shell command in the world root (`--cwd` to change it); exits with the command's code. `--timeout` in seconds (default 60, most 600). |
| `worlds sandbox check <id>` | Run the push gate on the tree without committing; exit `1` when a commit would be refused. `baseMoved` says the branch moved since your base. |
| `worlds sandbox import <id> <path>` | Import a rows file in the sandbox tree into the platform world the sandbox was opened on, as a data-only version; the sandbox then stands on that version. `--entity`, `--mode append\|replace`, `--per-chunk`, `--dry-run`, `--report <file>` (every refused row as JSONL, also when `--per-chunk` lands the other chunks). Refused rows exit 2, all of them counted; a `--per-chunk` import that lands the other chunks exits 0. Not `commit`, which versions source only. |
| `worlds sandbox commit <id> -m <message>` | Commit the tree as a new version on the base's branch (or `--branch`). `--rebase` replays your edits onto the branch when it moved. `--wait` caps how long it follows a data migration (seconds, default 1800). |
| `worlds sandbox close <id>` | Close the sandbox and release its workspace. Refused while the tree holds uncommitted edits; `--force` discards them. Closing twice is fine. |

```bash theme={null}
id=$(gateway worlds sandbox open acme-world@main | jq -r .id)          # open prints the sandbox as JSON
gateway worlds sandbox write "$id" verifiers/ticket_closed.sql --from ./ticket_closed.sql
gateway worlds sandbox exec "$id" 'ls verifiers' --timeout 30
gateway worlds sandbox check "$id" && gateway worlds sandbox commit "$id" -m "close-ticket verifier"
gateway worlds sandbox close "$id"
```

### When the branch moved while you worked

A commit lands on the version the sandbox opened on. If someone pushed to that branch since, a plain `commit` is refused with `base_moved` and nothing is written. Commit again with `--rebase` to replay your edits onto the branch's newest version:

```bash theme={null}
gateway worlds sandbox commit "$id" -m "close-ticket verifier" --rebase
```

Edits to different files, or to different lines of one file, merge. A file both sides changed in the same lines is a conflict: the command exits `1`, writes nothing, and prints one line per file on stderr — `conflict: <path> (<reason>)`, where the reason is `both_modified`, `modify_delete`, `binary` or `too_large`. Make those files agree with the branch, then commit with `--rebase` again.

### When a commit migrates the world's data

A contract change that only adds to the stored entities (a new entity, or a field that is not required) carries the world's imported rows to the new contract. That runs as a job: `commit` prints the batch, follows it with one line per phase on stderr, and then prints the new version. If the job fails, `commit` exits `1` and names why; nothing is committed and the tree is as you left it. With `--wait 0` it returns at once; follow the batch with `gateway worlds data status <batch> --world <slug>`. While the batch runs, another `commit` or `import` on the sandbox is refused (`sandbox_commit_pending`, naming the batch). Once it lands, a sandbox used in the last 15 minutes takes the new version right away, unless you changed a file the migration rewrites since the commit; otherwise its next `commit`, `import` or `exec` takes it and tells you what it found (`close` takes it without a word). `sandbox check` shows that version and the batch without changing anything.

A `--rebase` merges from your sandbox's base when the branch was built from it, or when any edit session that carried that base was closed into `main`, including one that landed it again or built on it. When the newest such session was discarded, it merges from the last version both share. When that session is still open, still closing, or stopped in conflict, the commit is refused (`rebase_unrelated`) and names the session and what to do: close an open one (`gateway worlds edit close <slug> --session <id>`), wait for one that is closing, and close one in conflict over `main` (`--force`) or discard it (`gateway worlds edit discard <slug> --session <id>`); then commit with `--rebase` again.

### Closing keeps your work safe

`sandbox close` refuses while the tree holds edits you have not committed. It exits `1`, closes nothing, and names up to ten of the files:

```
2 uncommitted file(s) (verifiers/ticket_closed.sql, tools/notify.py): commit them (gateway worlds sandbox commit <id> -m "<why>") or discard them (gateway worlds sandbox close <id> --force)
```

Commit what you want to keep, or close with `--force` to throw it away. A sandbox nobody touches for seven days is closed by the platform; `status` shows the deadline, and any verb other than `status` or `list` moves it.

### If the sandbox restarts

The machine behind a sandbox can stop. The platform then starts a new one and puts your tree back before anything else runs: from the latest automatic snapshot, or from the version you opened on when there is none. The next command tells you once, exits `1` and does nothing else:

```
... -> 409: sandbox_recreated: sandbox <id> stopped and was replaced by a new one. Its tree was restored from the snapshot taken at 2026-01-01T10:00:00.000Z; edits made after that are gone. ... (sandbox_recreated)
```

Look at the tree (`sandbox check <id>`), redo what is missing, and carry on; the next command runs normally. A snapshot taken before your last commit is not used: you get the committed version instead. If the snapshot cannot be read back, the command answers `sandbox_restore_failed` (HTTP 503) and changes nothing; run it again in a moment. `close` closes a restored sandbox that holds nothing new; if it holds uncommitted edits, `close` answers `sandbox_recreated` once and then refuses as usual. If a command deletes the world root itself, `check`, `commit` and `import` answer `sandbox_tree_missing`; close the sandbox and open a new one.

<Info>
  Commit before you leave a sandbox, or run `sandbox check <id>` to see what is still pending.
</Info>

## Serve and run

| Command | What it does |
| - | - |
| `worlds serve <path>` | Serve the world locally: its tools over `/__session/*`, with `connector.toml` its vendor API, and with `[ui]` its page on its own port at `/` (the layout the host uses, so any frontend built for the root works; also at `/__ui/` on `--port`; the page's `api_path` calls are answered with the pin). `--ui-port` sets the page's port (default the next one). `--task` opens the session on one task and prints its prompt; `--scenario` picks the starting state; `--port` sets the local port (default `8080`) and is refused by name when another process holds it (with its pid when `lsof` can see it); `--api-key` pins the credential the local server accepts. The runtime closes when the process that started it is gone, so a killed CLI leaves nothing on the port. |
| `worlds call <path, url or session id> <tool>` | Call one tool. A directory opens a fresh session for the call; a served url acts on the running local session; a session id (what `worlds session open` prints) acts on that hosted session through the platform. `--args` takes JSON, `@file` or `-`; `--state`, `--grade` and `--seed` read or set the session around the call; `--close` (url or id) ends the session. |
| `worlds run <world>` | Run a TypeScript agent module against hosted sessions and print the graded results. |

With a local `python3` 3.12 or newer, `serve` runs the world here in the foreground from the bundled runtime. Without one it opens a hosted api session instead and prints the session id and URL, which you close with `worlds session close`.

```bash theme={null}
gateway worlds serve ./acme-world --port 8080
gateway worlds run ./acme-world --agent ./agent.mjs --all
gateway worlds run acme-world@main --agent ./agent.ts --task acme-alice-dedupe
```

An outside agent acts on the served session. `GET /__session/task` returns the task id, prompt, served tools with each tool's description and input schema, and the verifiers that grade: the root world's under `verifiers`, each linked world's under `worlds.<alias>`. `POST /__session/call` takes `{"tool", "args"}`; a linked world's tool is `<alias>.<tool>`. `POST /__session/grade` scores every world on its own state and keys a dependency's verifier `<alias>/<verifier>`. `POST /__session/reset` and `POST /__session/seed` restore or replace the state, and `POST /__session/close` ends the session.

```bash theme={null}
gateway worlds serve ./whatsapp --task escalate_locked_account --port 8080
curl -s -X POST localhost:8080/__session/call -d '{"tool": "slack.post_message", "args": {"message": {"...": "..."}}}'
gateway worlds call http://localhost:8080 list_conversations --grade
gateway worlds call http://localhost:8080 slack.list_channels --args '{"workspace_id": "T01GATEWAY"}' --close
gateway worlds call ws_01J9… list_conversations --state          # a hosted session, through the platform
```

The grade names every world: `{"reward": 1.0, "rewards": {"escalated_to_slack": 1.0, "state_present": 1.0, "slack/state_present": 1.0}}`.

`worlds run` takes a `<world>` that is either a directory or a platform `slug[@ref]`. `--agent`/`-a` is `file.mjs[#export]`, the same form `task run` takes: the module's default export, or the export `#name` picks, is `async (session, task) => void`; `.ts` works when `tsx` is installed. Choose the work with `--task`/`-t` for one task or `--all` for the whole set, pin with `--version-id`, add surfaces with `--surface`/`-s`, and bound the wait with `--timeout-min`. Each task opens one live session and the task's own grader scores the end state.

## Test a world, and gate a pull request

A world ships tests for itself under `tests/`: `*.json` HTTP cases shaped `{name, request, expect}`, and pytest files that receive `WORLD_URL` and `WORLD_API_KEY`.

| Command | What it does |
| - | - |
| `worlds test <path>` | Run the suite against the served world. `--scenario` starts in a named scenario, `--out` writes the full JSON report, `--json` prints the report instead of lines. Exit `1` when any test fails. |
| `worlds ci <path>` | Push the checked-out world, run every task, and exit `1` below `--min-score`. |

```bash theme={null}
gateway worlds test ./acme-world
gateway worlds test acme-world@<versionId>
gateway worlds ci ./acme-world --agent ./agent.mjs --min-score 0.7
gateway worlds ci ./acme-world --run-config nightly --min-score 0.7
```

`test` takes a world directory or a platform `slug[@versionId]` to run the suite on the platform. With `[actions] on_push = ["tests"]` in `gateway-env.toml`, the platform runs the same suite on every push, and `gateway bench tests <slug>` reads those runs.

`worlds ci` is the pull-request gate in one command. It pushes to the world the directory tracks (the slug in `.gateway/.env-metadata.json`, so a world created as `acme-f7` stays `acme-f7`; pyproject's name is only the fallback for an untracked directory), pushes the tree as it is, exactly as `bench push` does, and runs the tasks the platform lists for the pushed version. Its branch defaults to `GITHUB_HEAD_REF`, then `GITHUB_REF_NAME`, then the directory's recorded branch, and it writes a table into `$GITHUB_STEP_SUMMARY` under GitHub Actions. `--run-config <name>` dispatches a hosted run config against the pushed version instead of running a local `--agent`; `--summary-file` also writes the JSON summary to a path; `--branch`/`-b` and `--message`/`-M` control the push.

## Hand an agent the packaged guide

`worlds skill [name]` prints one of the packaged skills, and `--install <repo>` writes it into that repository's `.claude/` along with any agent it ships. See [the CLI overview](/cli#print-a-packaged-skill-for-an-agent) for the five names.

`--install <repo>` with no name installs every packaged skill. `--list` prints the names and exits, and `--force` overwrites a file whose content differs.

```bash theme={null}
gateway worlds skill --install .
```

## Environment variables these commands read

Three variables beyond the credentials and runtime ones on [the CLI overview](/cli) change how `gateway worlds` behaves.

| Variable | What it does |
| - | - |
| `GATEWAY_ENV_FILE` | A path to a `KEY=value` file loaded before anything reads the environment. Blank lines, `#` comments, an `export ` prefix, spaces around `=`, and single- or double-quoted values are all accepted; a line without `=` is an error naming its number. A variable already set in the shell wins, and vendor keys named by `connector.toml` ride along, so a local capture sees them. |
| `GATEWAY_PROGRESS` | `1` forces the phase lines `worlds data import` and `worlds data check` print on stderr, `0` silences them. Without it they appear only on a terminal, which keeps CI logs clean. |
| `GATEWAY_WORLD_DATA_POLL_MS` | The first pause, in milliseconds, between polls while a data batch is followed. The default is `1000`; a value that is not a non-negative number falls back to it. |

## Where to go next

* [gateway benchmarks](/cli/benchmarks) — the same commands in benchmark order: tasks, k rollouts per model, results.
* [bench & versions](/cli/bench) — publishing the bundle, branches, tags and hosted runs.
* [Put data in a world](/worlds/data) — the rows file contract in full.
* [Mock any vendor API](/worlds/custom-connections) — `connector.toml`, captures, handlers and conformance.
* [Getting started with worlds](/worlds/getting-started) — the same commands as one walkthrough.


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