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

# Link worlds and resolve entities across them

> Declare how one world's entities map onto another's, normalize values that are not stored the same way, and ask one question across every linked world from the plaintext you know.

One person, device or account usually sits in several vendors under different keys, different
formats, and different redaction salts. A link declares how two worlds line up, and a
cross-world query asks both at once.

## Declare a link

A link is a `[dependencies]` entry plus an `entities` table mapping a local field onto the
dependency's field. `[dependencies]` lives in `gateway-env.toml`, the world's manifest, not in
`connector.toml`.

```toml filename="gateway-env.toml" theme={null}
[dependencies.spycloud]
pin = "spycloud-world@main"

[dependencies.spycloud.entities]
"people.email" = "spycloud_breach_records.email"
"people.phone" = { to = "spycloud_breach_records.phone", transform = "digits" }
"people.name"  = { to = "spycloud_breach_records.full_name", transform = ["nfkc", "trim", "collapse_ws", "lower"] }
```

The pin resolves once, at commit, to an exact version of the dependency. The ref after `@` is a
branch or tag name (`main` is every world's default branch, so `@main` is its tip at that
moment and never moves afterwards), a content-hash prefix, or a semantic version. Every mapping is
checked then too: both entities and both fields must exist in their worlds' contracts, or the
commit is refused naming what is missing. A link crosses projects inside your organization and
stops at its edge.

`gateway worlds describe <slug>` and `GET /api/public/worlds/{slug}` list a world's links with
their mappings.

## Transforms, for values that are not stored the same way

Two worlds seldom hold one value identically: one keeps a phone as bare digits, a client sends
`+1 (559) 779-1433`. A transform normalizes the plaintext on both sides before each world's
redaction is applied. Transforms are a closed, named set, applied left to right:

| Transform | Does |
| - | - |
| `lower`, `upper`, `trim`, `collapse_ws` | case, surrounding whitespace, runs of whitespace to one space |
| `nfkc` | Unicode compatibility normalization, so `ﬁrst` and `first` match |
| `digits` | keep `0-9` only; `+1 (559) 779-1433` becomes `15597791433` |
| `email_local_part`, `email_domain` | the part before, or after, the last `@` |
| `strip_prefix:<p>` | drop a fixed prefix once, such as `acct-` |
| `sha1`, `sha256` | the vendor-style hex digest, for a store that holds vendor hashes |

Any client can reproduce a transform from the manifest alone. An unknown transform is refused at commit. The same set exists in the
TypeScript SDK, with shared test vectors.

## Ask across worlds

```bash theme={null}
gateway worlds data query people-world people --where phone="+1 (559) 779-1433" --resolve --across --compare
```

`--across` runs the query in the world you name and in every linked world that maps the fields
you passed. For each linked world it takes your plaintext, applies the mapping's transforms,
turns the result into the form that world stores under its own `[redact]` policy, and queries
it. One header line per world on stdout, then its rows; each group also reports the chain it
applied as `transformed`. Use `--json` to pipe the output, since the headers share stdout with
the rows. The world you name is queried as given, without transforms: a transform belongs to
the mapping into a dependency.

`--compare` adds the parity table: per field and per world, whether anything matched, and the
transform that was applied. A field with no mapping to some world is reported as `unmapped`,
never an error.

The same question is `GET /api/public/worlds/{slug}/data?entity=&where=&resolve=true&across=true&compare=true`,
`queryRows(slug, entity, { where, resolve: true, across: true, compare: true })` in the
TypeScript SDK, and `query_world_data({ ..., across: true, compare: true })` for an agent.

<Info>
  Resolution happens at query time, from the plaintext you supply. Nothing is joined on disk.
  Each world salts its digests with its own slug, so the same email is a different digest in
  two worlds. Recomputing both forms from the plaintext is what makes the link work without a
  shared salt, a re-ingest, or digests anyone holding a snapshot could join.
</Info>

## Where to go next

* [Put data in a world](/worlds/data) for `data query`, `--where` and `--resolve` on one world.
* [Describe a world](/sdk-ts/describe) for how each field is stored and what resolves.


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