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

# Simulations for your users

> Serve a sealed replica to each of your own customers — a world per tenant, or one world with a task per end user — provisioned from one API call and graded per run.

Hand a world to your own customers, one sealed copy each, hosted on your account rather than
theirs. Provision a world per customer, open a session per end-user run, and grade the result.
Every copy stays sealed from every other.

## Pick the shape that matches what differs per customer

| What differs between your customers | Shape | How it is built |
| - | - | - |
| The data and the configuration — each tenant has their own records | **A world per tenant** | `gateway worlds create` once per customer |
| Only the goal — same system, different thing to accomplish | **One world, a task per end user** | One world, an inline task spec at session open |

Both shapes can be combined: a world per tenant, and a task per end user inside it.

<Info>
  Start with one world and per-user tasks. Move to a world per tenant only when
  customers genuinely need different data or a different contract, because every
  world is a version history you then have to keep.
</Info>

## Shape one: a world per tenant

### Create one world per customer

`gateway worlds create` cuts a world from a shipped connector template, from a connector
your organization published, or from a directory holding a tree you authored.

```bash theme={null}
export GATEWAY_HOST=https://withgateway.ai
export GATEWAY_PUBLIC_KEY=pk-lf-...
export GATEWAY_SECRET_KEY=sk-lf-...

gateway worlds create acme-crm \
  --from salesforce \
  --name "Acme CRM" \
  --rows acme-rows.json \
  --message "provisioned for Acme"
```

| Flag | What it takes |
| - | - |
| `--from` | A shipped template slug (`gateway worlds schema templates` lists them), `workspace:<slug>` for a connector published in your organization's hub, or a world directory |
| `--overlay` | A directory of files laid over the source before the tree is compiled — one tenant's customization of a shared base |
| `--rows` | A rows file seeded into the first version; repeatable |
| `--entity` | The entity that bare-row files hold |
| `--name`, `--description` | What the world is called in the app |
| `--message`, `-m` | The change reason recorded on the first version |
| `--batch` | A JSONL file of worlds to create in one call |
| `--json` | Print the created world as JSON |

The slug must be URL-safe and unique within the project, so prefix it with the tenant:
`acme-crm`, `globex-crm`. The world is stored as its schema tree, and when the connector
declares routes (`connector.toml`) the same session serves them beside the tools with no
second step.

### Provision many tenants in one call

`--batch` takes a JSONL file (or `-` for standard input) of one world per line, and prints
one result line for each.

```jsonl theme={null}
{"slug": "acme-crm", "from": "salesforce", "name": "Acme CRM"}
{"slug": "globex-crm", "from": "salesforce", "name": "Globex CRM"}
{"slug": "initech-crm", "from": "workspace:our-crm", "name": "Initech CRM"}
```

```bash theme={null}
gateway worlds create --batch tenants.jsonl
```

A line may also carry `overlay` and `data` (the seed rows, as
`{"rows": {"<entity>": [...]}}`). The command exits 0 when every world was created, 2 when
any line failed, and 1 on an error before the batch started.

### Or call the API directly

`POST /api/public/worlds` is the same request, for provisioning from your own backend.

```bash theme={null}
curl -sS -X POST "$GATEWAY_HOST/api/public/worlds" \
  -u "$GATEWAY_PUBLIC_KEY:$GATEWAY_SECRET_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
        "slug": "acme-crm",
        "name": "Acme CRM",
        "from": { "connector": "salesforce" },
        "data": { "rows": { "accounts": [{ "id": "A1", "name": "Acme" }] } },
        "message": "provisioned for Acme"
      }'
```

The response carries `slug`, `containerId`, `kind`, and the first `version` with its content
hash. A slug already in use answers 409; a tree the contract refuses answers 400 and names the
path that failed.

### Load each tenant's own records

```bash theme={null}
gateway worlds data import acme-crm acme-rows.json --message "Acme Q3 accounts"
gateway worlds data import globex-crm globex-rows.json --message "Globex Q3 accounts"
```

Each import pushes a new version of that tenant's world. Rows are checked against the
world's contract first, so one bad row writes nothing and the refusal names the field. See
[Put data in a world](/worlds/data) for the rows file and the append and replace modes.

<Info>
  Hash or drop personal fields before loading records from a real customer
  system. A world is sealed from the outside, but it is still readable by
  everyone in the project that owns it.
</Info>

## Shape two: one world, a task per end user

One published version serves any number of end users at once, each with their own goal,
starting rows and grader. Supply the task when the session opens instead of publishing it in
the bundle.

```python theme={null}
import os

from gatewaysdk.world_sessions import open_session

assert os.environ.get("GATEWAY_SECRET_KEY"), "set GATEWAY_SECRET_KEY"


def run_for_user(user_id: str, invoice_id: str) -> dict:
    task = {
        "name": f"mark-paid-{invoice_id}",
        "instruction": f"Mark invoice {invoice_id} as paid.",
        "seed": {"rows": {"invoices": [{"id": invoice_id, "status": "open"}]}},
        "grader": {
            "kind": "assertions",
            "checks": [
                {"entity": "invoices", "where": {"id": invoice_id, "status": "paid"}}
            ],
        },
        "metadata": {"externalUserId": user_id},
    }
    with open_session("acme-billing", task, surfaces=["api"]) as session:
        session.ready()
        run_my_users_agent(session.api, session.api_headers, session.instruction)
        return session.grade()


print(run_for_user("u_42", "INV-7"))
```

The same spec works from the CLI as `--task-file`, and over HTTP as the `task` field of
`POST /api/public/world-sessions`.

```bash theme={null}
gateway worlds session open acme-billing --task-file user-42.json --surface api
```

| Spec field | What it does |
| - | - |
| `instruction` | What the agent is told to accomplish |
| `seed` | `{mode?, rows}` applied to the clone after the snapshot |
| `grader` | `{kind: "assertions", checks}` or `{kind: "python", source}` |
| `metadata` | Echoed back on the session — where your own user id belongs |
| `name` | A label for the task, used in results |

<Info>
  Inline and stored tasks run on the World Host engine, which serves worlds stored as trees,
  with or without routes.
</Info>

### Tasks bundled in the world

A task can ship in the world instead of the request. Two formats exist, read by different engines:

| Format | Files | Graded by |
| - | - | - |
| Scenario task | `scenarios/<name>/scenario.toml`, `seed/*.json`, `instruction.md`, `checks/verify.py` | The World Host |
| Verifier task | `tasks.json` (or `[[tasks]]` in `gateway-env.toml`) + `verifiers/*.sql` | The tools engine: local sessions, and a world stored as a tree (`gateway worlds create --from`) |

`gateway worlds session open <world> -t <name>` opens either kind by name. Details: [the two bundled task formats](/worlds/tests#tasks-are-not-tests-the-two-bundled-formats).

### Keep a task if the same user runs it again

Store a task your users return to in the project rather than in a request body. Stored tasks
live outside any bundle, are versioned, and can be filtered by their metadata.

```bash theme={null}
gateway worlds task create user-42.json --world acme-billing -n mark-paid-INV-7
gateway worlds task list --world acme-billing --metadata externalUserId=u_42
gateway worlds session open acme-billing --task-id <taskId> --surface api
```

`worlds task validate` checks a task's seed and grader entities against a world version
without opening a session. `worlds task update` makes a new version; sessions already open
keep the copy they started with.

## The whole loop, per end-user run

<Steps>
  <Step title="Open a session for the run">
    ```bash theme={null}
    gateway worlds session open acme-crm --task-file run-8817.json --surface api
    ```
  </Step>

  <Step title="Hand your user's agent the URL and the token">
    The session's `api.url` and `api.token` are all an agent needs. Point it there instead of at
    the real vendor; nothing else in its code changes.
  </Step>

  <Step title="Grade the end state">
    ```bash theme={null}
    gateway worlds session grade <sessionId>
    ```
  </Step>

  <Step title="Close it, and keep the ledger">
    ```bash theme={null}
    gateway worlds session export <sessionId> --out run-8817.jsonl
    gateway worlds session close <sessionId>
    ```
  </Step>
</Steps>

## What gets recorded, and to whom

Worlds and versions created with an API key are attributed to the key: the first version
records `apikey:<id>` as its author and the world has no human owner. Everything lands in the
project the key belongs to, so one project is one customer-facing integration.

Runs record the version they faced, so a result stays interpretable after the tenant's world
moves on. Old versions stay addressable, and `--version-id` on `session open` pins a run to
an exact one.

## Verify

**List what you provisioned.** `gateway worlds list` prints each world's id, slug and latest
version; `--slug` narrows it to one.

**Check a tenant's data landed.** `gateway worlds data show acme-crm` prints the world's kind
and entity counts, and `--entity accounts` prints that entity's rows.

**Prove the seal between tenants.** Open a session on two tenants' worlds and read the state
of each; neither should contain the other's records.

```bash theme={null}
gateway worlds session state "$ACME_SESSION"
gateway worlds session state "$GLOBEX_SESSION"
```

**Watch them live.** Each world's Overview lists its live sessions with engine, state, task,
API URL and age, plus Pin and Stop.

<Info>
  Slugs are unique per project, so a tenant name that collides answers 409
  rather than overwriting anything. Decide the naming scheme before you
  provision the first customer — renaming a world later does not move the runs
  that recorded its old identity.
</Info>

## Where to go next

* [Spin worlds up and down](/worlds/sessions) for the session lifecycle and pinning.
* [Run many sessions at once](/worlds/parallel) for many end-user runs at the same time.
* [Put data in a world](/worlds/data) for the rows file and live seeding.
* [Getting started](/worlds/getting-started) to author the world the tenants are cut from.


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