Replaces the v1.1 'world_id is a string' model with a graph-of-planes. Driven by Kay's Q2 ('worlds are planes') and the v1.2 design review.
- 17-planes.md (NEW): the plane taxonomy, the four relation types, Cypher patterns, migration from world_id, open questions
- 01-ontology.md: Plane and Setting as first-class nodes; the 6 plane-relation edge types
- 02-time-model.md: plane-aware time (entity_planes_at_time as the 6th time-aware primitive)
- 08-architecture.md: data flow for plane questions (the 'can I get from X to Y' pattern)
- 11-extensibility.md: how to add custom planes and plane-relations without code
- 12-storage-strategy.md: planes are pure graph (no Postgres/Redis/Qdrant/S3 changes)
- 14-examples.md: example 5 — full Setting + planes + Roland + Asmodeus + LLM tool calls
- README.md: v1.2 entry + doc 17 in the table of contents
The POC rebuild (T10) is the next step: migrate the existing 4 world_id values to Setting/Plane nodes and update the plugin queries to use EXISTS_IN/LOCATED_IN.
18 KiB
17 — Planes of Existence: A First-Class Graph Model
Status: v1.2 amendment (replaces v1.1's flat
world_idstrings with graph nodes). Drives: Q1, Q2, Q3 from the design review (planes as first-class nodes; "worlds are planes";EXISTS_INis a timeless type-assertion; the four plane-relation edge types).
This document is the new plane model. It supersedes the v1.1 "world_id string namespace" pattern. The change is motivated by the limitations of the flat-string model when faced with D&D-style cosmology: a string can name a plane, but it cannot model the relations between planes (Shadowfell reflects Material, Astral is a transit layer, the Nine Hells has nine sub-layers).
After this amendment:
Planeis a first-class graph node.- A
Settingis a parent that owns its planes. - Every entity has zero or more
EXISTS_INedges toPlanenodes (timeless type-assertion). - Every entity has zero or more time-bounded
LOCATED_INedges toPlanenodes (where it was at time T). - Planes have relations to other planes:
REFLECTS,LAYER_OF,ADJACENT_TO,ACCESSIBLE_VIA. - The old
world_idstring property is deprecated but kept for read-only compatibility during the migration window.
The key insight: "worlds are planes"
In v1.1 we used world_id as a flat string on every node. The values in the seed data were default, arda_greyscale, mardonar, voldramir. The flaw: a "world" was a single string with no internal structure, so it could not represent the relations between worlds.
In v1.2 the model is rewritten: a "world" is just the primary Material Plane of a setting. When a player says "Mardonar," they mean the Material Plane of the Mardonari setting — not a separate universe. The same person can be in Mardonar's Material Plane today, in Mardonar's Voldramir demiplane next week, and in the Mardonari setting's Astral transit layer in between. The plane changes; the person does not get duplicated.
This is the D&D-canonical model. It also happens to be what world_id was trying to express all along, just with the wrong abstraction.
Node labels (new)
Setting
A campaign, game, or world scope. The top of the world hierarchy. A Setting is the unit of "what world are we talking about" — Eberron, the Forgotten Realms, Mardonari, the Iron Kingdoms, a home-brew demiplane-only setting.
| Property | Type | Notes |
|---|---|---|
id |
string | Stable slug, e.g. eberron, mardonari, default (legacy alias for the v1 default world). |
name |
string | Human-readable, e.g. "Eberron," "Mardonari." |
summary |
string | One-paragraph world description. |
genres |
string[] | e.g. ["high_fantasy","steampunk","noir"]. |
canonical_time |
string | The current in-fiction time of the campaign. Format: {era}.{unit} (see 02-time-model.md). |
A Setting is the root of the plane tree. Every Plane belongs to exactly one Setting via HAS_PLANE. A Setting with no planes is valid (and represents a world whose plane structure is not yet detailed).
Plane
A layer of existence within or alongside a setting. The engine treats all planes uniformly — there is no special-cased Material plane. Planes are typed by their kind property, and relations between planes are first-class edges.
| Property | Type | Notes |
|---|---|---|
id |
string | Stable slug within the setting, e.g. eberron.material, mardonari.voldramir, default.arda_greyscale. |
name |
string | Human-readable, e.g. "The Material Plane," "Voldramir," "Mount Celestia." |
kind |
string enum | See the taxonomy below. |
summary |
string | Optional one-paragraph description. |
parent_plane |
string (Plane.id) | For layered planes (Nine Hells → Dis → Minauros, etc.). |
accessible |
boolean | Whether mortals can reach the plane without artifact/spell assistance. False for the Far Realm, true for Material. |
alignment_tendency |
string | Optional, e.g. lawful_good, chaotic_evil, neutral. |
valid_from |
string | When this plane came into existence. Format: {era}.{unit}. |
valid_until |
string | When it ceased to exist. null for still-active planes. |
The plane taxonomy (the kind enum)
The engine does not hard-code any specific planes. kind is a free-form string, but the recommended taxonomy is the D&D-style cosmology:
kind |
Meaning | Examples | Notes |
|---|---|---|---|
material |
The primary, baseline plane. The world players normally inhabit. | Eberron Material, Mardonari Material, Forgotten Realms Material. | Every setting has one. The "world" colloquially. |
reflection |
A plane that mirrors the material plane but with different qualities. | Shadowfell (dark), Feywild (vibrant), the parallel Material of the Arda greyscale seed. | Has a REFLECTS edge to a material plane. |
transit |
A plane used to travel between other planes. | Astral Plane, Ethereal Plane. | Has ADJACENT_TO edges to other planes. |
ethereal |
A plane that overlaps the material — you can see into both. | The Ethereal Plane. | Distinct from transit because the overlap is geometric, not journey-based. |
outer |
A plane inhabited by deities, fiends, celestials, or other extraplanar beings. | Mount Celestia, the Nine Hells, the Abyss, Mechanus, the Beastlands, Gehenna. | Grouped by alignment tendency. |
inner |
A plane embodying an elemental or quasi-elemental force. | Plane of Fire, Plane of Water, Plane of Earth, Plane of Air, Para-elemental planes. | Often paired. |
demiplane |
A small, finite, often personalized plane. | A wizard's pocket dimension, Voldramir, the demiplane created by Mordenkainen's Magnificent Mansion, Ravenloft (a demiplane that imprisons). | Has a contained_in (often Material) and is finite in size. |
transcendent |
A plane that exists outside the standard cosmology. | The Far Realm (lovecraftian outer), the realm of the gods-as-concept (in settings where "the gods" is a place, not beings). | Reserved for cosmology-bending settings. |
A setting does not need every kind. A low-magic home-brew world might have only material + a single demiplane. A planes-heavy D&D setting has dozens.
Edge types (plane model)
HAS_PLANE (Setting → Plane)
Every Plane belongs to exactly one Setting. This is the root of the plane tree.
(:Setting {id: 'eberron'})-[:HAS_PLANE]->(:Plane {id: 'eberron.material', kind: 'material'})
EXISTS_IN (any entity → Plane) — timeless type-assertion
This edge asserts that the entity type exists in the plane. It is not time-bounded (see Q3). It answers "can this kind of thing be in this plane?" not "was it there at time T?"
(:Person {name: 'Asmodeus'})-[:EXISTS_IN]->(:Plane {id: 'eberron.nine_hells', kind: 'outer'})
(:Person {name: 'Aldric'})-[:EXISTS_IN]->(:Plane {id: 'eberron.material', kind: 'material'})
(:Person {name: 'Roland Raventhorne'})-[:EXISTS_IN]->(:Plane {id: 'mardonari.material', kind: 'material'})
EXISTS_IN is many-to-many. A god exists in multiple planes (their home, the Astral, possibly Material). A mortal typically exists in one. A plane-transcendent entity exists in many. This is the answer to "how does EXISTS_IN work for entities that exist in multiple planes?" — see "Open questions" below for the trade-off.
The relation is optional. A setting with no EXISTS_IN edges from a node means "this node's plane presence is not specified yet" (provisional). The consistency engine has a rule for this: see 04-consistency.md.
LOCATED_IN (any entity → Plane) — time-bounded
This edge says "the entity was at this plane at time T." It is time-bounded via valid_from and valid_until, exactly like the other time-bounded relations in the engine. It answers "where was Roland in year 312 TA?" not "what kind of plane is Roland from?"
(:Person {name: 'Roland Raventhorne'})-[:LOCATED_IN {
valid_from: '3rd_age.year_425',
valid_until: '3rd_age.year_445'
}]->(:Plane {id: 'mardonari.material'})
(:Person {name: 'Roland Raventhorne'})-[:LOCATED_IN {
valid_from: '3rd_age.year_428',
valid_until: '3rd_age.year_446'
}]->(:Plane {id: 'mardonari.voldramir', kind: 'demiplane'})
The same Roland can be in Material and a demiplane at different times. The Cypher pattern:
// Where was Roland in 430 TA?
MATCH (p:Person {name: 'Roland Raventhorne'})-[r:LOCATED_IN]->(plane:Plane)
WHERE time_in_window('3rd_age.year_430', r.valid_from, r.valid_until)
RETURN plane.name, plane.kind
REFLECTS (Plane → Plane) — material mirrors
A plane that is a reflection of another. Shadowfell reflects Material, Feywild reflects Material, the Arda greyscale "world" reflects Eberron's Material. A reflection is a parallel universe that mirrors the geography of its source but with twisted or inverted qualities.
(:Plane {id: 'eberron.shadowfell', kind: 'reflection'})-[:REFLECTS]->(:Plane {id: 'eberron.material'})
(:Plane {id: 'eberron.feywild', kind: 'reflection'})-[:REFLECTS]->(:Plane {id: 'eberron.material'})
This is directed. Eberron's Shadowfell reflects Eberron's Material, not Forgotten Realms' Material. The plane-id is the disambiguator.
LAYER_OF (Plane → Plane) — sub-planes
A plane that is a sub-layer of a larger plane. The Nine Hells is a single Outer Plane, but it has nine layers (Avernus, Dis, Minauros, Phlegethos, Stygia, Malbolge, Maladomini, Cania, Nessus). A layer LAYER_OF its parent plane. This is also how a demiplane LAYER_OF its containing plane if you prefer that model.
(:Plane {id: 'eberron.dis', kind: 'outer'})-[:LAYER_OF]->(:Plane {id: 'eberron.nine_hells', kind: 'outer'})
(:Plane {id: 'mardonari.voldramir', kind: 'demiplane'})-[:LAYER_OF]->(:Plane {id: 'mardonari.material', kind: 'material'})
A plane can have many layers. A layer can be a parent of further layers (Dis has sub-layers in some cosmologies). The relation is not required — a flat taxonomy is valid.
ADJACENT_TO (Plane → Plane) — geometric neighbors
A plane that is geometrically adjacent — you can physically reach it by walking, climbing, swimming, or by being there at the right moment. Material is adjacent to Ethereal (the Ethereal Plane overlaps it). Material is adjacent to the Astral in some cosmologies. Adjacency is the answer to "can I walk there from where I am?"
(:Plane {id: 'eberron.material'})-[:ADJACENT_TO]->(:Plane {id: 'eberron.ethereal', kind: 'ethereal'})
(:Plane {id: 'eberron.material'})-[:ADJACENT_TO]->(:Plane {id: 'eberron.astral', kind: 'transit'})
ADJACENT_TO is symmetric. If Material is adjacent to Ethereal, Ethereal is adjacent to Material. The Cypher pattern uses undirected traversal.
ACCESSIBLE_VIA (Plane → Spell OR Plane → Item OR Plane → Location)
A plane that is reachable by a specific mechanism. The most common form is spell: Material is ACCESSIBLE_VIA Plane Shift, Astral Projection, or Gate. It is also valid to point at a physical portal: the Nine Hells is ACCESSIBLE_VIA the Stygian Gate in Dis.
(:Plane {id: 'eberron.nine_hells'})-[:ACCESSIBLE_VIA]->(:Spell {name: 'Plane Shift'})
(:Plane {id: 'eberron.material'})-[:ACCESSIBLE_VIA]->(:Location {name: 'The Stygian Gate'})
This is the "is there a way to get there" relation. It is also the LLM-friendly one: "I'm at X, can I get to Y?" becomes a graph traversal.
The canonical plane taxonomy (a starter set for D&D-style settings)
This is a recommended starting set, not a hard requirement. The engine does not require any of these planes to exist.
| Plane | kind | reflects | adjacent_to | layer_of | notes |
|---|---|---|---|---|---|
{setting}.material |
material | — | ethereal, astral | — | Required. |
{setting}.shadowfell |
reflection | material | ethereal | — | Optional. Dark mirror. |
{setting}.feywild |
reflection | material | — | — | Optional. Vibrant mirror. |
{setting}.ethereal |
ethereal | — | material, shadowfell | — | Overlaps material. |
{setting}.astral |
transit | — | material, outer planes | — | Transit layer. |
{setting}.mount_celestia |
outer | — | astral | — | Lawful good afterlife. |
{setting}.nine_hells |
outer | — | astral | — | Lawful evil afterlife. |
{setting}.abyss |
outer | — | astral | — | Chaotic evil. |
{setting}.mechanus |
outer | — | astral | — | Lawful neutral. |
{setting}.beastlands |
outer | — | astral | — | Chaotic neutral. |
{setting}.far_realm |
transcendent | — | — | — | Lovecraftian outer. |
The {setting} prefix is a convention, not a constraint. A setting with one plane can use the bare name (material instead of eberron.material).
Cypher patterns
"What planes does this entity exist in?"
MATCH (p:Person {name: 'Asmodeus'})-[:EXISTS_IN]->(plane:Plane)
RETURN plane.id, plane.kind, plane.name
"Where was this entity at time T?"
MATCH (p:Person {name: 'Roland Raventhorne'})-[r:LOCATED_IN]->(plane:Plane)
WHERE time_in_window('3rd_age.year_430', r.valid_from, r.valid_until)
RETURN plane.id, plane.kind, plane.name
"What planes are accessible from where I am?"
MATCH (me)-[:LOCATED_IN {valid_until: null}]->(here:Plane)
MATCH (here)-[:ADJACENT_TO|ACCESSIBLE_VIA*1..2]->(target:Plane)
WHERE target <> here
RETURN DISTINCT target.id, target.kind, target.name
"What is the demiplane of Voldramir, and what contains it?"
MATCH (d:Plane {id: 'mardonari.voldramir'})-[:LAYER_OF]->(parent:Plane)
RETURN d.name AS demiplane, parent.name AS contained_in
"What reflections of Material exist in this setting?"
MATCH (material:Plane {kind: 'material'})<-[:REFLECTS]-(reflection:Plane)
WHERE material.id STARTS WITH 'eberron.'
RETURN reflection.id, reflection.kind
Migration from v1.1
The v1.1 design used world_id: string on every node as a flat namespace. The v1.2 model replaces that with the EXISTS_IN graph traversal. The migration is:
-
Add
SettingandPlanenodes for every distinctworld_idvalue currently in the graph.defaultbecomesSetting("default")+Plane("default.material", kind: 'material').mardonarbecomesPlane("mardonari.material", kind: 'material')underSetting("mardonari").voldramirbecomesPlane("mardonari.voldramir", kind: 'demiplane')underSetting("mardonari").arda_greyscalebecomesPlane("default.arda_greyscale", kind: 'demiplane')underSetting("default"). -
Create
EXISTS_INedges from every entity to its primary plane. For most nodes this is a single edge. For multi-plane entities (gods, outsiders), it is many. -
Mark the legacy
world_idproperty deprecated but do not delete it. Plugin queries check forEXISTS_INfirst, fall back toworld_idif the new edges are not present (during the migration window). -
Update plugins and the seed to use the new model. The seed script writes the planes; the plugins query by plane.
-
Remove
world_idproperty in v2.0. After the migration is stable, the string property is dropped. Plugin queries that still reference it are bugs.
Open questions and trade-offs
EXISTS_IN is many-to-many
Default choice (Q-clarify default): a node can have EXISTS_IN edges to many planes. The trade-off:
- Many-to-many (default). A god exists in their home plane, the Astral, and possibly Material. A mortal exists in one plane but can be extended to many. Most flexible, but means a "what planes is this entity in" query can return many rows and the LLM has to disambiguate.
- One-to-one. Each entity has a single primary plane. If it also exists in others, that is a
LOCATED_INedge (time-bounded). More restrictive, matches D&D "type" thinking ("Aldric is a Material-Plane creature"). - One or more, but at least one. Same as many-to-many, but missing
EXISTS_INis a consistency violation rather than a provisional state.
The default is reversible — it is a Cypher pattern, not a schema migration. If a downstream user (the LLM, the world-builder) finds many-to-many cluttered, switching to one-to-one is a refactor of the seed and a small plugin change. The design note here is: when in doubt, support the more general case and let the LLM narrow.
Layer-of vs contained-in
A demiplane is contained in a plane (Voldramir is contained in Mardonari's Material). The taxonomy uses LAYER_OF for both "sub-layer" (Dis is a layer of the Nine Hells) and "contained in" (Voldramir is a layer of Mardonari's Material). An alternative is to add a CONTAINED_IN edge, but the dual semantics of LAYER_OF ("a sub-plane") cover both. The Cypher remains the same.
Plane relations that aren't in this doc
If the world needs more plane-edge types (e.g., BORDERS, TRADE_ROUTE, WAR_WITH), they are added to the ontology the same way other edge types are. The four in Q4 (REFLECTS, LAYER_OF, ADJACENT_TO, ACCESSIBLE_VIA) are the recommended starting set.
What this is not
- Not a renaming of
world_idtoplane_id. The new model is a graph of planes with relations, not a string namespace. The migration is structural, not cosmetic. - Not D&D-specific. The taxonomy is D&D-flavored because the engine's use case is high fantasy, but the model works for any cosmology: the Wheel of Time has its own pattern (the Pattern, the One Power, the Dark One's prison — a
transcendentplane), Malazan has its warrens, Cosmere has its three realms. The taxonomy is a recommendation, not a constraint. - Not finished. The taxonomy will grow as the engine is used. The kind enum, the relation types, the time-boundedness of
EXISTS_IN(currently timeless), the migration timeline — all of these are open to revision in v2 based on what the LLM and the world-builder actually do with the model.