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

# The files a world is made of

> A reference for the three files you edit by hand — schema/world.json and its x-gateway block, gateway-env.toml, and connector.toml — with every key, its default, and what the compiler refuses.

A world directory holds three authored files and a set of generated ones. The keys below are
the ones you edit by hand.

| File | Decides |
| - | - |
| `schema/world.json` | the data model: entities, fields, keys, relationships, tool signatures |
| `gateway-env.toml` | what kind of world it is, which tools it serves, what runs on push, what it links to |
| `connector.toml` | the vendor connection: auth, capture, redaction, and the routes the world answers |

`connector.toml` is optional. A world with no vendor to mirror declares its tools in
`schema/world.json` and serves them without it.

## `schema/world.json` is a JSON Schema document with one extra block

The file is a standard JSON Schema 2020-12 document. Reusable shapes go in `$defs`; the
world's own declarations go in a top-level `x-gateway` object.

```json theme={null}
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "urn:gateway:connector:vendor-world",
  "$defs": {
    "Record": {
      "type": "object",
      "properties": {
        "id": { "type": "string" },
        "email": { "type": "string" },
        "updated_at": { "type": "string", "format": "date-time" }
      },
      "required": ["id", "email", "updated_at"],
      "additionalProperties": false
    }
  },
  "x-gateway": {
    "version": 1,
    "domain_version": "1.0.0",
    "package": { "id": "vendor-world", "version": "1.0.0" },
    "entities": {
      "records": {
        "schema": { "$ref": "#/$defs/Record" },
        "primary_key": ["id"],
        "unique": [["email"]],
        "relationships": []
      }
    },
    "tools": {}
  }
}
```

`$schema` must be exactly `https://json-schema.org/draft/2020-12/schema`. `$id` must be an
absolute, fragment-free URI; nothing ever fetches it, so a `urn:` is fine. References resolve
only to `#/$defs/<name>` in the same file or `sibling.json#/$defs/<name>` next to it — remote
references are refused.

### Every `x-gateway` member is required

| Member | Type | Must be |
| - | - | - |
| `version` | integer | exactly `1` |
| `domain_version` | string | non-empty, at most 256 characters |
| `package` | object | `{id, version}`, both non-empty strings, no other keys |
| `entities` | object | at least one entity |
| `tools` | object | present; `{}` when the world serves no tools |

Unknown members are refused by name, not ignored. A world that serves an HTTP API rather than
tools still declares `"tools": {}`.

### An entity names a closed object and its keys

| Key | Required | What it holds |
| - | - | - |
| `schema` | yes | the row shape, inline or `{"$ref": "#/$defs/Name"}` |
| `primary_key` | yes | a non-empty list of distinct direct property names |
| `unique` | no | a list of key lists, each a further uniqueness constraint |
| `relationships` | no | a list of foreign-key declarations |

An entity root must be a direct closed object: `"type": "object"` with
`"additionalProperties": false` and no `allOf`, `anyOf` or `oneOf` at the root. Reuse a shape
through `$ref`, never through composition — the compiler turns the root into the
database engine's columns.

Primary-key fields must be direct property names, listed in the schema's `required`, typed
`string` or `integer`, and never nullable. `unique` keys may reach nested values, written as a
JSON Pointer such as `/owner/user_id` — a dotted path is refused with a message saying so.

### Entity and field names are SQL-safe, with reserved prefixes

Entity names match `[A-Za-z][A-Za-z0-9_]*`. Field names match `[A-Za-z_][A-Za-z0-9_]*`. Both
refuse the prefixes `sqlite_`, `gateway_` and `benchmark_`; an entity name may not start with
`_`, and a field may not be called `_key` or `payload`. Two entities whose names differ only
in case collide under SQL case folding and are refused.

### A relationship is restrict-only

```json theme={null}
"relationships": [
  { "fields": ["owner_id"], "target": "users", "on_delete": "restrict" }
]
```

All three keys are required. `on_delete` must be `"restrict"`, so delete dependents explicitly,
in one batch. The target must be a declared entity
whose primary key has the same number of fields and matching types, and one field tuple may
name only one target.

### A tool declares its input and output schemas

```json theme={null}
"tools": {
  "list_records": {
    "input": {
      "type": "object",
      "properties": { "kind": { "type": "string" } },
      "required": ["kind"],
      "additionalProperties": false
    },
    "output": { "type": "array", "items": { "$ref": "#/$defs/Record" } },
    "description": "Records of one kind."
  }
}
```

Tool names match `[A-Za-z][A-Za-z0-9_.-]*`. `input` and `output` are required and `description`
is optional text; the input must resolve to an object schema. Every name declared here must
also appear in `[tools] serve` in `gateway-env.toml`, exactly — a mismatch in either direction
fails the compile.

### The compiler writes five files back

`gateway worlds schema compile` regenerates the paths below from `schema/world.json`. Edit the
source and recompile; never edit a generated file.

| Path | What it is |
| - | - |
| `schema/lock.json` | the contract's hashes: domain, package, inputs, tools, each artifact |
| `db/schema.sql` | the storage DDL the world's engine runs (SQLite today) |
| `schema/runtime.json` | the compiled contract the runtime reads |
| `schema/README.md` | a per-entity reference table, generated for humans |
| `tool-schemas.json` | the tool input and output schemas on their own |

<Info>
  `gateway worlds schema entity add` scaffolds an entity into `schema/world.json` for you,
  `gateway worlds schema entity remove` takes one out once nothing refers to it, and
  `gateway worlds schema reconcile` rewrites the contract to match what a vendor actually
  returned in `captures/`.
</Info>

## `gateway-env.toml` says what kind of world this is

`gateway worlds schema init` writes the manifest below. The compiler or the platform reads
every table in it.

```toml filename="gateway-env.toml" theme={null}
[environment]
name = "vendor-world"
kind = "mock-tools"
db = "sqlite"
connector = "custom"

[schema]
language = "gateway-world/1"

[tools]
serve = ["list_records", "get_record"]

[db]
engine = "sqlite"

[actions]
on_push = ["tests"]
```

| Table and key | Accepted values | Notes |
| - | - | - |
| `[schema] language` | `"gateway-world/1"` | exact match; the table is also how the platform recognizes the directory as a schema world |
| `[environment] kind` | `"mock-tools"` | `[env]` is accepted as the table name too |
| `[environment] db` | an engine name; `"sqlite"` when omitted | the engine factory decides which names exist; SQLite is the one shipped today. Must name the same engine as `[db] engine` |
| `[db] engine` | an engine name; `"sqlite"` when omitted | the engine factory decides, as above. `db.schema`, `db.seed` and `db.path` are refused — the generated paths are fixed |
| `[tools] serve` | list of tool names | required, even as `[]`. Must match `x-gateway.tools` exactly: same names, no repeats, nothing extra |
| `[actions] on_push` | list; `"tests"` runs the world's tests on push | see below |
| `[dependencies]` | alias tables | see below |

A directory counts as a schema world only if its manifest carries `[schema]`. Drop the table
and the world stops compiling.

### `[actions] on_push` runs the world's own tests on every push

```toml filename="gateway-env.toml" theme={null}
[actions]
on_push = ["tests"]
```

When `"tests"` is listed, every pushed version has
the suite in `tests/` run against it and the run lands on the version for the Tests tab and
`gateway bench tests` to read. A push never waits on its tests and never fails because of them.
See [Test a world](/worlds/tests).

### `[dependencies]` links this world to another

```toml filename="gateway-env.toml" theme={null}
[dependencies]
drive = "google-drive@c71381c3"          # alias = "<slug>@<ref>"

[dependencies.sheets]
pin = "google-sheets@0.2.1"
task = "default"
tools = ["read_range", "append"]

[dependencies.sheets.entities]
"people.email" = "contacts.email"
```

A link is a manifest edit, so it mints a version and shows up in History. [Link worlds together](/worlds/links) has the
mapping grammar, the transform set and the cross-world query.

## `connector.toml` describes the vendor

One file serves three jobs: what `gateway worlds connector capture` calls, what
`gateway worlds serve` answers, and what the hosted world answers. [Mock any vendor
API](/worlds/custom-connections) walks through writing one; the tables below list every key.

### `[connector]` identifies the connection

| Key | Required | Default | Notes |
| - | - | - | - |
| `slug` | yes | — | lowercase letters, digits and hyphens. **Also the redaction salt** — see [Keep real data out](/worlds/redaction) |
| `base_url` | yes | — | an `http`/`https` origin with an optional path, and no query or fragment |

### `[auth]` names the credential without holding it

| Key | Required | Default | Notes |
| - | - | - | - |
| `scheme` | yes when the table exists | `none` when `[auth]` is omitted | `header`, `bearer`, `basic`, `query`, `oauth2`, `none` |
| `name` | for `header` and `query` | — | the header or query parameter carrying the secret |
| `secret_env` | for every scheme but `none` | — | the environment variable holding the credential; the value itself is never written here |
| `headers` | no | `{}` | more credential headers: `{ "X-App-Token" = "VENDOR_APP_TOKEN" }`, each value an environment variable name. May not repeat the secret header |
| `token_url` | no, `oauth2` only | `/oauth/token` | a path under `base_url` or an absolute URL |
| `client_id_env` | for `oauth2` | — | environment variable naming the client id |
| `scope` | no, `oauth2` only | — | the scope asked for |

`token_url`, `client_id_env` and `scope` are refused on any scheme but `oauth2`.

### `[capture]` paces the real vendor and follows its cursor

| Key | Default | Notes |
| - | - | - |
| `requests_per_minute` | `30` | pacing between requests |
| `timeout_seconds` | `30` | per-request timeout |
| `max_pages` | `5` | how many pages one capture follows |
| `cursor_param` | — | the request-side parameter the next cursor is sent in |
| `cursor_field` | — | the response body field holding the next cursor; `""` or `null` ends paging |
| `cursor_header` | — | the response **header** holding the next cursor, for vendors that page that way |

`cursor_field` and `cursor_header` are mutually exclusive, and `cursor_param` must be paired
with one of them.

### `[redact]` decides what a captured value becomes

| Key | Shape | Does |
| - | - | - |
| `hash` | list of names or dotted paths | replaces the value with `"sha256:<hex>"`, salted with `[connector] slug` |
| `drop` | list of names or dotted paths | removes the field |
| `preserve` | list of names or dotted paths | hashes while keeping length and character classes, so format-bounded fields still validate |
| `round` | `{ field = decimals }`, 0–10 | keeps a number, loses its precision |

A field belongs to exactly one of the four. [Keep real data out](/worlds/redaction) has the
path grammar, the precedence order, the salt rule, and which commands apply redaction and
which do not.

### `[[operations]]` declares one route

```toml filename="connector.toml" theme={null}
[[operations]]
name = "get_records_by_email"
method = "GET"
path = "/records/by-email/{email}"
params = ["since", "severity[]"]
entity = "records"
results = "results"
```

| Key | Required | Default | Notes |
| - | - | - | - |
| `name` | yes | — | snake\_case, unique across the file |
| `method` | no | `GET` | `GET`, `POST`, `PUT`, `PATCH`, `DELETE` |
| `path` | yes | — | starts with `/`, no query or fragment. `{name}` marks a path argument |
| `params` | no | `[]` | query parameter names; a `[]` suffix means the vendor repeats the key |
| `entity` | no | — | the entity captured rows land in on ingest |
| `results` | no | `""` | dotted path to the row array; `""` means the body is the row |

A name may not be both a path argument and a query parameter.

### `[operations.ingest]` reshapes vendor rows into your entities

| Key | Shape | Does |
| - | - | - |
| `drop` | list of field names | removes vendor fields the entity does not carry |
| `carry_args` | `{ row_field = "arg" }` | writes a capture's path argument or query parameter onto each row, for a value the vendor echoes nowhere in the body |
| `[[operations.ingest.explode]]` | `field`, `entity`, `carry`, `take` | turns a nested array into rows of a child entity |

Every source named in `carry_args` must be a path argument or a declared parameter of the same
operation, and any ingest projection requires the operation to name an `entity`.

### `[operations.api]` decides how that route is served

| Key | Default | Notes |
| - | - | - |
| `action` | `read` | `read`, `create`, `update`, `delete`, `handler` |
| `filter` | `{}` | `{ arg = "row.field.path" }`; `update` and `delete` require one |
| `match` | `{}` | per filter argument: `equals`, `contains`, `prefix` |
| `handler` | — | required by `action = "handler"`, refused otherwise; a function in the handlers module |
| `status` | `200`, or `201` for `create` and `204` for `delete` | must be 2xx |
| `envelope` | inherits `[api] envelope` | |
| `single` | `true` for `create` and `update`, else `false` | answer the object rather than a list |
| `body` | `{}` | `{ row_field = "body.path" }`; `create` and `update` only |
| `defaults` | `{}` | constant fields written on write; `create` and `update` only |
| `generate` | `{}` | `{ id = "uuid" }` — `uuid`, `int`, `now`; `create` and `update` only |
| `sample` | `{}` | `{ arg = "a value the route accepts" }`. **What lets `connector conform` call the route**; every key must be a path argument or a parameter |
| `page` | inherits `[api] page` | `none`, `offset`, `cursor` |
| `not_found` | inherits `[api] not_found` | a JSON string; `""` means no body |
| `not_found_status` | inherits `[api] not_found_status` | 400–599 |
| `cursor_absent_when_done` | inherits `[api] cursor_absent_when_done` | |

### `[api]` sets the world-wide envelope, paging and errors

| Key | Default | Notes |
| - | - | - |
| `envelope` | `""` | the key rows come back under; `""` answers the array itself |
| `hits_key` | `"hits"` | the count's key; `""` leaves the count out |
| `cursor_key` | `"cursor"` | the next cursor's key in the envelope |
| `cursor_absent_when_done` | `false` | drop the cursor key on the last page instead of sending `""` or `null` |
| `previous_key` | — | emit the previous page's cursor under this key, the way Django REST Framework answers |
| `page_headers` | `{}` | `{ cursor = "Cursor", total = "Total-Count" }` — keys are `cursor`, `total`, `previous` only |
| `page_links` | `false` | next and previous as absolute URLs of the served host |
| `page` | `"none"` | `none`, `offset`, `cursor` |
| `page_size` | `100` | positive integer |
| `limit_param` | `"limit"` | |
| `offset_param` | `"offset"` | |
| `cursor_param` | `"cursor"` | request-side parameter names for the paging style |
| `unauthorized` | `{"error": "unauthorized"}` | a JSON string replayed as given; `""` means no body |
| `unauthorized_status` | `401` | 400–599 |
| `not_found` | `{"error": "not_found"}` | as above |
| `not_found_status` | `404` | 400–599 |
| `invalid` | `{"error": "invalid"}` | the body for a rejected request |
| `invalid_status` | `400` | 400–599 |
| `error_content_type` | `"application/json"` | for vendors that answer errors as text or XML |
| `token_path` | `auth.token_url`, else `/oauth/token` | the path the world mints bearers on; `oauth2` only |
| `handlers` | `api/handlers.py` | the module `handler` and `before` resolve against; must exist if either is used |
| `before` | — | a function run ahead of every route, for per-key quotas and allow-lists |

### `[ui]` ships a dashboard with the world

A world serves the `ui` session surface only when it declares one. The table points at a
**prebuilt** directory — the World Host serves those files directly, and a world whose UI
exists only as a container image needs a pod instead. The shipped templates declare one, and
`gateway worlds schema compile` generates the directory from the contract; see
[Give a world a UI](/worlds/ui).

```toml filename="connector.toml" theme={null}
[ui]
static = "ui/static"
entry = "/"
api_path = "/__world-api"
generated = true
```

| Key | Required | Default | Notes |
| - | - | - | - |
| `static` | yes | — | a directory inside the world holding `index.html`. Relative, no `..` segments |
| `entry` | no | `/` | the page an agent lands on first; starts with `/` |
| `api_path` | no | `/__world-api` | the same-origin prefix the page calls the API at, so browser-side code never needs the API's origin. Starts with `/` and may not be `/` alone |
| `generated` | no | unset | `true`: compile writes the directory from the contract; `false`: hand-written, compile leaves it; unset: compile writes it only when it holds no page |

<Info>
  Build the UI into a directory such as `ui/static`. `bench push` drops `dist`, `build`,
  `outputs`, `__pycache__` and dot-directories from every bundle as build artifacts, so
  `static` pointing inside one is refused at validation rather than failing every session.
</Info>

## Where to go next

* [Mock any vendor API](/worlds/custom-connections) for writing `connector.toml` step by step.
* [Keep real data out](/worlds/redaction) for the full `[redact]` model.
* [Link worlds together](/worlds/links) for `[dependencies]` mappings and transforms.
* [Test a world](/worlds/tests) for the suite `[actions] on_push` runs.
* [Getting started with worlds](/worlds/getting-started) for the path from an empty directory.


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