IDs, Names and Tags
Every Scene Entity carries three names. They are not synonyms, they are not interchangeable, and exactly one of them is an address.
| Field | For | Inspector field | Rules |
|---|---|---|---|
id |
Addressing. The only way anything resolves an entity. | not shown as an editable field | Assigned on creation, ≤96 characters, never changes |
label |
Machines. Uniqueness, scripts, mods, and — for kind: "card" — the card's identity |
Slug |
[a-z0-9-], unique in the scene, ≤80 characters |
displayName |
Humans. The Hierarchy row, status messages, the entity's engine name | Name |
Free-form, ≤80 characters, optional |
Neither name is an address.
idis.Nothing — no script, no mod, no runtime lookup — resolves an entity by either name.
id — the address#
id is assigned when the entity is created and never changes for the life of that entity.
It is what parentId points at, what an action targets, what a script or a mod receives
back from a query, and what the Hierarchy panel keys its rows on so selection and expansion
survive a rebuild.
You never type an id. There is no Inspector field for it, and there should not be —
editing an address is not an authoring operation.
Two properties make it the address and neither name a candidate:
- It is stable. Renaming an entity does not move it. Restoring a version does not renumber it. A host migration does not reassign it.
- It is unambiguous. Two entities can share a
displayNamequite legitimately. No two share anid.
Compare this with the engine's guid, which addresses a Live Entity and is not
stable — see Document vs engine.
label — the slug, for machines#
label is the entity's slug. It is lowercase, dash-separated, unique within the scene, and
it is what you type into the Inspector's Slug field.
chess-board wood-table red-die-1 ace-of-spades
The editor normalizes whatever you type: the input is lowercased, accented characters are
folded to ASCII, every run of other characters collapses to a single dash, leading and
trailing dashes are trimmed, and the result is truncated. If that collides with an existing
slug, a numeric suffix is appended until it does not. So typing Chess Board!! into Slug
produces chess-board, and typing it again produces chess-board-1.
label is what non-human consumers use:
- Scene scripts and mods refer to entities by slug in the code you write.
- Default names generated on import are slugs (an imported
chess-set-1k-boardbecomeschess-board;wood_table_2kbecomeswood-table). - Uniqueness is enforced on the slug, not on the human name.
⚠ For a card, the slug is the card#
This is the one place where renaming has a consequence beyond readability.
For an entity of kind: "card", label is the card's identity, and card identity is
what drives hidden-information redaction — the mechanism that stops a peer from learning
which card is face-down in someone else's hand by reading the network traffic.
Changing a card's Slug changes which card it is. Not what it is called: which card it
is. A deck whose card slugs are rewritten is a different deck as far as identity-bearing
state is concerned.
This is exactly why label could never be relaxed to accept spaces and capitals.
Doing so would have made "rename this card so it reads nicely" and "change this card's
identity" the same gesture. Instead, the human name was given a field of its own.
Treat a card's slug as data, not as a caption. If you want the card to read differently in
the Hierarchy, change its Name.
What the schema actually enforces#
Worth knowing if you are hand-writing a setup file or generating entities:
- The schema constrains
labelto 1–80 characters and requires it to be present. The[a-z0-9-]shape is produced and maintained by the editor's slug normalization, not by a schema pattern. - Nothing DiceyTable generates will produce a
labeloutside that shape, and every tool that consumes a slug assumes it. Author slugs as slugs.
displayName — for humans#
displayName is free-form: spaces, capitals, punctuation, up to 80 characters. Chess Board, Player 1's Hand, Big Red Die. It is optional.
It is what you see in:
- the Hierarchy row,
- the Inspector's
Namefield, - status messages and the context menu,
- the Live Entity's
pc.Entity.name.
When it is absent or blank, the slug is shown instead. One resolver — objectDisplayName()
— decides this everywhere, so the Hierarchy row, the entity name, the status line and the
context menu can never disagree about what something is called.
Clearing the Name field does not leave the entity nameless; it falls back to the slug.
The Name is replicated — and, for a card, redacted with the card#
Everything a player can see, they can also read: displayName rides in every snapshot, and
whatever you type in Name reaches every peer entitled to that entity.
For kind: "card" the host redacts it exactly as it redacts the card's identity. When a viewer
may not see a card's face — it is face down and unowned, or it is in someone else's hand, and it
is not revealed to that viewer's team — the wire copy that peer receives has label rewritten to
Card, metadata.cardId removed, metadata.__redacted set, and displayName deleted. Every
hidden card therefore looks alike, with no leftover signal that this one had a special name. Flip
the card up, deal it into that player's hand, or reveal it to their team and the name comes back
along with the face, because it is the same entitlement.
So naming a card Ace of Spades is safe, and it is what the field is for. What the Name field
is not is a hiding place:
- it is public whenever the card is public — you cannot use it for something that must stay secret after a reveal;
- no other kind's
displayNameis redacted at all. A token namedTraitor Markerannounces itself to everyone, always.
The sanctioned place for "what is this really" is secretMetadata: a per-entity property
bag, capped at 2 KiB of JSON, that the host strips from every viewer not entitled to the entity's
identity — for a deck and for every non-card kind, that means every viewer except the host. See
Object state.
This is a fix, not a long-standing rule. Until 2026-07-28 redaction neutralized
labelandmetadata.cardIdand never looked atdisplayName, so a face-down card namedAce of Spadeswas broadcast intact to every peer. If you authored around that by keeping card names deliberately vague, you no longer need to.
The rule: editing Name leaves Slug alone#
The Inspector shows Name and Slug as two fields, one above the other, and they are
independent:
Editing the Name leaves the Slug alone once the Slug has been customized.
Setting a human name never touches the slug. That is the guardrail that makes the card rule
above safe: renaming an entity for readability can never silently change a card's identity,
because renaming does not write to label at all.
The converse is also true and worth stating: editing Slug does not update Name. If
you have set a human name, that is the name; the slug moves underneath it.
| You edit | displayName |
label |
|---|---|---|
Name |
changes | untouched |
Slug |
untouched | changes (normalized, de-duplicated) |
Tags#
Tags are how you group entities without parenting them. A tag is a short lowercase token, an entity can carry many, and membership is by tag rather than by containment — which is what makes a Tag Group different from a Folder or a parent.
Tags are useful because they cost nothing structurally: an entity can be in a parent assembly and in five tag groups at once, and tag-based lookup is the mechanism both the editor's filter and the scripting surfaces are built on.
Author tags#
| Rule | Value |
|---|---|
| Character class | [a-z0-9_-] — letters, digits, underscore, dash |
| Length | 1–32 characters |
| Maximum per entity | 100 |
| Case | Stored lowercase; input is lowercased and trimmed |
| Duplicates | Collapsed, order preserved |
The tag character class includes underscore while the slug character class
does not. my_tag is a valid tag; my_slug is not a valid slug.
Tags are edited from the TAGS chip input in the ENTITY header, and drive the
Hierarchy's tag filter pills, Select all with tag, and tag-scoped group editing.
The reserved dt: namespace#
Tags beginning dt: are platform-owned and unauthorable. Not "discouraged" —
unrepresentable.
The author-tag pattern is:
/^[a-z0-9_-]+$/i
: is deliberately not in that character class. A platform tag therefore cannot be
expressed as an author tag, which means an author or a mod can never forge one. A
schema parse of ["dt:internal"] fails outright; it is not accepted and quietly ignored,
it is rejected.
That guarantee is what the platform tags are worth having:
| Tag | Applied to |
|---|---|
dt:internal |
Every entity DiceyTable creates that is not an authored Scene Entity — debug overlays, gizmo proxies, ghosts, previews, snap markers, joint holders, per-entity Face/Back children, imported-model sub-roots |
dt:object |
Every table-entity root |
dt:kind:<kind> |
dt:kind:card, dt:kind:die, … — one per Platform kind |
dt:seat-group |
Seat-zone and seat-label entities, alongside the user-visible seat-group tag |
dt:child |
Any entity parented to another table entity |
If an author could forge dt:internal, they could hide an entity from the editor. If they
could forge dt:object, they could make something claim to be a table entity that is not.
The regex is the whole separation. It is not widened.
In the editor, platform tags are shown dimmed and cannot be removed.
dt: tags appear in read results#
This is the part that catches people out.
Platform tags are unauthorable, but they are not invisible. They are real tags on the Live Entity, and they come back when you enumerate that entity's tags in the editor or the runtime. Any code there — a filter, a count, a "does this entity have any tags?" check — must account for them, or it will see tags it did not put there.
Scripts are the exception, and the distinction matters.
dt:tags live on the Live Entity, not in the scene document. A table script'sObjectData.tagsis the authored tag set from the object's definition, andtableObjectStateSchemavalidates tags against/^[a-z0-9_-]+$/, which forbids:. SoObjectData.tagscan never contain adt:tag and a script-side tag filter can never match one. Account for platform tags when you are reading the entity graph; do not defend against them in a script.
Two specific cases:
dt:childis added and removed automatically by parenting. Parent an entity and it gainsdt:child; unparent it and it loses it. Nobody typed it and nobody can.- A tag filter with an unsatisfiable needle matches nothing, not everything. Asking for
{ tags: ["dt:internal"] }does not return the whole table just because the needle is invalid — that would be the opposite of what was asked. An empty needle list, on the other hand, means "no filter" and matches everything.
Tags are for grouping, names are not#
The engine's own guidance, which DiceyTable follows: tag-based searches are faster and more flexible than deep name-based searches, and nothing should identify an entity by a name prefix.
DiceyTable used to detect internal entities by a __DiceyTable name prefix. That was
replaced by dt:internal precisely so names could stay readable while detection moved off
them. The rule that came out of it:
An Entity's name is for humans; its
idis for machines. Nothing addresses an entity by name — scripts and mods address byidor by tag.
Summary#
idaddresses. It is stable, unique, never typed, and the only thing that resolves an entity.labelis the slug: the uniqueness key, the machine-facing handle, and for a card its identity, which drives hidden-information redaction. Edit it inSlug, and for cards edit it with care.displayNameis the human name. Optional, free-form, shown everywhere a person reads it, falls back to the slug. It is replicated, and on acardit is redacted together with the card's identity — safe to write, not a hiding place. Real secrets go insecretMetadata.- Editing
Namenever touchesSlug. - Tags group entities by membership rather than containment.
dt:-prefixed tags are platform-owned, unforgeable — and still present in anything that reads tags.
See also#
- Object state — every replicated field, and
secretMetadata's per-kind redaction rule. - Mod scripting API —
TableObjectState.labelis the slug a mod reads;displayNameis optional and often absent. ObjectHandle—ObjectData.namecarries the slug, not the display name. It is the single thing scripts get wrong about this page.- Types —
SpawnObjectOptions.namesets the slug too, not the human name. world—world.getAllObjects({ tag }), filtering on the tags defined here.
