Tags and Groups
Tags group entities by membership rather than containment — the mechanism behind Tag
Groups, the Hierarchy's tag filter pills, and
GROUP EDIT's tagged scope. This page is the working-with-it companion to IDs, names and
tags; read that page first for the full rules on
the author-tag character class and length limits.

Authoring tags#
The TAGS chip input lives in the ENTITY header. Type a tag and press enter or comma to
add it; click the × on a chip to remove it. The Hierarchy's tag filter pills and Select all with tag both read from the same tag set every entity carries.
Author tags match [a-z0-9_-], 1–32 characters, up to 100 per entity, stored lowercase and
de-duplicated (normalizeObjectTags, packages/shared/src/objectTags.ts).
The dt: namespace is unauthorable, not just discouraged#
Tags beginning dt: are platform-owned. You cannot type one into the TAGS chip input, and
you cannot construct one any other way an author or a mod controls:
OBJECT_TAG_PATTERN = /^[a-z0-9_-]+$/i
: is deliberately outside that character class (objectTags.ts), so a platform tag
cannot be expressed as an author tag at all. This is not soft validation that happens to
reject it — a schema parse of ["dt:internal"] fails outright
(tableObjects.ts, the tags field regex on every object-shape schema).
There is no code path where an author or a sandboxed mod script can forge dt:internal to
hide an entity from the editor, or dt:object to make something masquerade as a table
entity.
In the editor, platform tags are shown dimmed and cannot be removed — you can see them, you cannot edit them.
| 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> |
One per Platform kind — dt:kind:card, dt:kind:die, … |
dt:seat-group |
Seat-zone and seat-label entities, alongside the user-visible seat-group tag |
dt:child |
Any entity currently parented to another table entity |
dt:child is added and removed automatically by parenting#
You will never type dt:child and you cannot remove it by hand — it tracks parenting state
directly. Parent an entity and it gains dt:child; unparent it and it loses it. If you are
filtering or scripting against tags and see dt:child appear and disappear on an entity you
never tagged, that is this mechanism, not a bug.
Platform tags still appear in read results#
This is the detail that catches people out. Unauthorable does not mean invisible: dt:
tags are real tags on the Live Entity, and any code that reads that entity's tag set in
the editor or the runtime — a filter, a count, a "does this have any tags?" check — sees them
along with the author's own tags. A filter that assumes it only ever sees what an author
typed will be wrong.
Table scripts are the exception. ObjectData.tags carries the authored tag set from
the object's definition, and the schema validates tags against /^[a-z0-9_-]+$/, which
forbids :. A dt: tag can therefore never reach a script, and a script-side tag filter
can never match one. Defend against platform tags when reading the entity graph, not in a
script.
The corollary matters just as much: a filter for an unsatisfiable tag matches nothing, not
everything. Filtering for { tags: ["dt:internal"] } from outside — a mod, say — does not
fail open and return the whole table just because the needle is a forged/invalid tag; an
empty needle list is what means "no filter" (objectTagsMatch, objectTags.ts).
⚠ A verified case-sensitivity quirk — the stored form is the rule#
Two regexes govern tags and they do not quite agree, and it is worth knowing rather than being surprised by it:
OBJECT_TAG_PATTERN(objectTags.ts) is case-insensitive —/^[a-z0-9_-]+$/i.isUserTag("Blue")istrue.- The Zod
tagsfield on every object schema (tableObjects.ts) is not case-insensitive —/^[a-z0-9_-]+$/with noiflag.tableObjectStateSchema.parse({ …, tags: ["Blue"] })rejects"Blue".
In practice this never produces a surprising rejection, because nothing hands a raw,
un-normalized tag straight to the schema. normalizeObjectTags lowercases every incoming
tag before it is stored or validated (objectTags.ts: tag.trim().toLowerCase()),
so what actually reaches the schema is always already lowercase. Treat lowercase as the
rule: author tags are stored and compared lowercase, full stop, and the isUserTag /
schema discrepancy above only matters if you are calling one of these functions directly on
unnormalized input rather than going through the normal authoring or read paths.
Tag Groups vs Folders vs parenting#
A Tag Group (seat-group-0 in the Hierarchy, TYPE token tag group) is a set of
entities sharing a tag. It has no transform of its own, is not addressable, and — unlike
parenting — moving it moves nothing, because there is nothing to move: membership is
computed, not structural. See The Scene Model
for the full definition and how it differs from a Folder.
Bulk tag operations#
Select all with tag— from a tag filter pill, select every entity carrying that tag into the current multi-selection.SELECTION (N)'s Tag all / Untag all — apply or remove a tag across the current multi-selection. See Multi-select.GROUP EDIT'staggedscope — bulk show/hide, lock, scale and rotate every entity carrying the active tag filter, without needing a matching folder to exist. See Multi-select.
See also#
- IDs, names and tags — the full author-tag rules and why the character class excludes spaces.
- Multi-select — the bulk tag actions in
SELECTION (N). - Glossary
- Mod scripting API —
api.listObjects({ tag, tags, match }), the mod-side filter over these tags. ObjectHandle—ObjectData.tags, reading tags from a table script.- Types —
SpawnObjectOptions.tags, tagging an entity as you spawn it. world—world.getAllObjects({ tag }), which takes one tag and not a list.
