Identity bridges (resolve: chains) — link Person/Organization to any external system
A bridge is a virtualJoin to an external-identity node (GitHubIdentity, HubSpotOwner, …) from a canonical Person/Organization. Instead of persist+keyField, declare an ordered resolve: rule chain. At query time, for the anchors a query actually binds — any person/org, not just the connecting user — the host resolves the bridge lazily, first matching rule wins, and (with writeThrough) persists it so it’s reused next time. Downstream joins (e.g. RAISED on a resolved GitHubIdentity) then anchor on the now-real bridge.
- name: GitHubIdentity visibility: internal properties: login: { metadata: { identity: "true" } } # bridge MERGE key virtualJoins: - anchorLabel: Person relationship: HAS_GITHUB keyField: primaryEmail # anchor key the pre-pass probe reads recordKeyField: email # field on each resolved record that maps it back to the anchor writeThrough: true # persist the resolved bridge (default true); reused + respected refreshAfter: 30d # re-resolve a bridge older than this (optional; default 30d) resolve: # ordered; FIRST rule that yields a bridge (or finds one) wins - existingBridge # respect a fresh bridge already linked - learnedHandle: { property: githubLogin, as: login } # an explicit handle on the anchor (no email) - canonicalEmail: { producer: githubUsersByEmail } # resolve via the Person's email setRule kinds (host-provided, referenced by name; the chain is per-realm so the link key can vary and need not be email):
| rule | does |
|---|---|
existingBridge |
If a fresh bridge is already linked to the anchor (persisted, learned, or manually added), use it — stop. |
learnedHandle: { property, as } |
Read an explicit identity stored on the anchor (e.g. Person.githubLogin) → bridge { <as>: handle }. No email lookup. |
canonicalEmail: { producer } |
Resolve via the anchor’s canonical email set (host-owned: primaryEmail/email/emails) → call producer. |
canonicalDomain: { producer } |
Same for an Organization’s domain/domains. |
Canonical identity is host-owned — realms never hardcode email vs primaryEmail; the canonical* rules read the right properties for you.
Producer requirement for a canonical* rule: the producer must return records carrying the bridge’s identity property AND the matched recordKeyField. If the source can’t echo the key you searched by (e.g. GitHub search/users returns login but not the queried email), set echoKeyAs on the producer — it calls the op one key per call and stamps record[<echoKeyAs>] = <that key>:
# producers/github.yml — search returns login, NOT the queried email → echo it- name: githubUsersByEmail kind: remote operation: search/users args: { q: "{keys}" } keyTemplate: "{key} in:email" # q = "<email> in:email" (one email per call) echoKeyAs: email # stamp the queried email onto each {login,...} hit records: "$.items[*]"Most producers don’t need echoKeyAs — e.g. HubSpot owners/contacts records already carry email. A canonical* rule whose producer op has no gateway tool simply yields nothing (the chain falls through); wire the op (in apis/) or supply a learnedHandle for those anchors.
(persist: true without a resolve: chain is the older form: an eager enrichment over all anchors that commits the bridge. It also takes the join OUT of the query path, so until enrichment has run the traversal answers nothing and asks nothing — which is why it is refused on a join whose target nothing anchors on. Prefer resolve: — it’s lazy, works for any anchor, and self-heals. See VIRTUAL_CYPHER §5.2.1.)

