<!-- https://minecraftdocs.dev/systems/foundations/tags — this page as markdown. The book's index: https://minecraftdocs.dev/llms.txt; the whole book in one file: https://minecraftdocs.dev/llms-full.txt -->

# Tags

> Verified against **Minecraft 26.3** · Part II · A parrot flies down through the canopy looking for a log to perch on, a moment after a data pack put a new block in the logs tag.

A parrot is looking for somewhere to sit. `Parrot.ParrotWanderGoal` scans
the blocks around it and asks the block state beneath each one whether it is
leaves or *in `BlockTags.LOGS`* — a leaves block it recognises by class, a log
only by tag. Nothing in that goal names oak, or spruce, or the
block a data pack added an hour ago; the tutorial toast that tells a new
player to punch a tree asks the same question. A tag is a named set of
registry entries, defined in data-pack JSON and tested against by key, and
it is how data reaches into hard-coded behaviour without the behaviour
naming any specific block. [The registries page](identifiers-and-registries#the-freeze-rule-stated)
ended on a promise: a frozen registry's *contents* never change. Its tags
do. Type `/reload` with a data pack that adds a block to *logs* and the
parrot perches on it a moment later — with `BuiltInRegistries.BLOCK` frozen
since before the title screen. Nothing is unfrozen to do it, and no lock is
taken; the swap is safe for a reason that has nothing to do with either.

## The cast

| class | what it decides | thread |
|---|---|---|
| `TagKey` | the name: a registry key and an `Identifier`, interned so equal keys are one object | any |
| `TagFile` · `TagEntry` | the on-disk shape: a list of entries, each an element or a reference to another tag, and the *replace* flag | — |
| `TagLoader` | reads every pack's copy of every file and resolves them in dependency order; what an id resolves to is its `TagLoader.ElementLookup` | Worker at world load, Server on `/reload` |
| `MappedRegistry` | the tag half: `MappedRegistry.frozenTags`, one `HolderSet.Named` per freeze-time key, and `MappedRegistry.allTags`, the bound `MappedRegistry.TagSet` | read from any; written before the freeze by the launching thread, which binds a static registry's tags to empty, and by a data-pack registry's load tasks, and after it by whichever thread is applying |
| `Registry.PendingTags` | a loaded table not yet installed, with a `Registry.PendingTags.lookup` that hands out the new table's tags, bound only when it is applied | built on a worker at world load, on the server thread for `/reload`, on the Render thread from the packet; applied on the owning thread |
| `HolderSet.Named` | one tag's contents, bound at each reload that keeps the tag | — |
| `Holder.Reference` | one element's own `Set` of `TagKey`s — the thing a membership test reads | — |
| `TagNetworkSerialization` | the wire form: tag id to a list of registry ints | Server encodes, client decodes |

## A tag is a key and a file

In code a tag is a `TagKey`: a record of the registry's `ResourceKey` and
an `Identifier`, interned through a weak interner so that `TagKey.create`
always returns the canonical instance and a membership test is a
set lookup on a canonical key. The keys are declared once, in catalogues that
hold no contents — `BlockTags`, `ItemTags`, `EntityTypeTags`, `BiomeTags`,
`FluidTags` and their siblings, twenty-one files in `net/minecraft/tags`
including `DamageTypeTags`, `EnchantmentTags`, `StructureTags`,
`PoiTypeTags`, `FeatureTags` and `TimelineTags`.
A key that a block and its item share is declared once as a
`BlockItemTagId` in `BlockItemTags` and projected into the catalogues that use it:
`BlockTags.LOGS` is the block half of `BlockItemTags.LOGS`.

On disk a tag is a `TagFile`: a list of `TagEntry` and a *replace* flag.
Each entry is an element id or a *#*-prefixed reference to another tag,
with a *required* flag that defaults to true. The file lives at
*data/\<namespace\>/tags/\<registry path\>/\<name\>.json* —
`Registries.tagsDirPath` builds that string in exactly one place, and the
registry path is singular: *tags/block*, *tags/item*, *tags/entity_type*,
*tags/worldgen/biome*. Vanilla's own files are written
by the data generator (`TagsProvider`, `TagBuilder`), which the running game
never calls, and players reach tags through the *#tag* syntax of
`ResourceOrTagArgument` and `ResourceOrTagKeyArgument` (Part XIII).
`ResourceSelectorArgument`, beside them, is a glob over ids and takes no
tag at all.

Between the two sits `TagLoader`, generic over what an id resolves to: a
`Holder` for a registry, a `CommandFunction` for function tags. It has two
instance steps. `TagLoader.load` reads every pack's copy of every file into
lists of `TagLoader.EntryWithSource`; `TagLoader.build` resolves them in
dependency order, `TagLoader.tryBuildTag` doing one tag at a time through
the `TagLoader.ElementLookup` the loader was built with. Its output, a map
from each tag to its holders, is resolved but bound to nothing. Nothing owns the
loader: it is called as static functions from `WorldLoader`,
`MinecraftServer.reloadResources`, `ReloadableServerRegistries`, the
registry load tasks and the client's `RegistryDataCollector`, with no manager
between them. Function tags are the exception — `ServerFunctionLibrary` is a real
reload listener and runs its own `TagLoader` inside it. Everything in
`net/minecraft/tags` ships in both jars.

Inside a `MappedRegistry` a tag is two things. `MappedRegistry.frozenTags`
holds one canonical `HolderSet.Named` per key for the tags that existed
when the registry froze, and `MappedRegistry.allTags` is a
`MappedRegistry.TagSet`, the bound view, which starts out as
`MappedRegistry.TagSet.unbound`, where every read throws. Beside the
registry, each `Holder.Reference` carries its own `Set` of `TagKey`s, bound
by `Holder.Reference.bindTags`; that set, not the registry, is what
`Holder.Reference.is` reads.

## The four moments tags are loaded

World load comes first, on the worker pool
([anatomy](../anatomy/anatomy#four-threads-worth-memorising)).
`TagLoader.loadTagsForExistingRegistries` runs *before* the reload
listeners, over the `RegistryLayer.STATIC` layer, and produces a
`Registry.PendingTags` for every static registry that has at least one tag
that builds. The pending tables are made visible to worldgen and
reloadable-registry loading
through `TagLoader.buildUpdatedLookups`, and applied, one registry at a
time, in `ReloadableServerResources.updateComponentsAndStaticRegistryTags`
once every listener has finished. For the whole of a load the old tags are
what `Registry.getTags` answers and the new table's names are what the
loading codecs see.

`/reload` is the same call on the server thread, handed the composite
access of `MinecraftServer.registries` — so it re-reads and re-applies tags
for the **dynamic** worldgen registries too, not only the static ones. Of
the loading paths only the reloadable registries re-run — loot tables,
recipes, advancements and the rest — and a worldgen registry keeps its
elements and gets new tags.

The third is a data-pack registry, which loads its tags inside its own load
task rather than through the mechanism above: `ResourceManagerRegistryLoadTask`
reads them after its elements, with `TagLoader.ElementLookup.fromGetters`,
and `RegistryLoadTask.registerTags` binds them under the registry's write
lock — before the freeze, so there is a lock here and no swap. (The
reloadable registries take this path too — they load through the same load
tasks — so a data pack's loot-table tags bind like any other's, and vanilla
ships none.)

The fourth moment is the client's. One `ClientboundUpdateTagsPacket`
covering every static and every synced registry is sent by `SynchronizeRegistriesTask` after
the registry data. In configuration the client merely *buffers* it —
`ClientConfigurationPacketListenerImpl.handleUpdateTags` hands it to
`RegistryDataCollector.appendTags`, and nothing resolves until
configuration finishes. After a server `/reload`, `PlayerList.reloadResources`
broadcasts the same packet into the play phase, and
`ClientPacketListener.handleUpdateTags` handles that one at once.

## From JSON to a parrot's decision

```mermaid
sequenceDiagram
    participant WL as WorldLoader
    participant TL as TagLoader
    participant MR as MappedRegistry
    participant RSR as Reloadable<br/>ServerResources
    participant Parrot as Parrot

    Note over WL,MR: world load, on the worker pool
    WL->>TL: loadTagsForExistingRegistries over the STATIC layer
    TL->>TL: load: every pack's tags/block/logs.json, lowest first
    TL->>TL: build: DependencySorter orders the nested tags, then tryBuildTag
    TL->>MR: prepareTagReload: a Registry.PendingTags, which Registry.getTags cannot see yet
    Note over WL,RSR: worldgen and reloadable-registry codecs see the new table through Registry.PendingTags.lookup
    Note over MR,RSR: the thread driving the load, after the last reload listener has applied
    RSR->>MR: Registry.PendingTags.apply: bind, swap, rebind every holder
    Note over Parrot: a later server tick, the parrot's wander goal
    Parrot->>Parrot: state.is(BlockTags.LOGS) — a set lookup on the holder's own tags
    Note over MR,Parrot: no arrow to the registry: the check never reaches it
```

*One tag, from the pack files to the check that uses it. The gap between
`MappedRegistry.prepareTagReload` and `Registry.PendingTags.apply` is where
the opening's riddle lives, a table built that `Registry.getTags` cannot yet
see, and the last note marks an absence: the parrot's check touches no
registry at all.*


### Every pack's file, lowest first

`TagLoader.load` asks
[the resource system](resource-system#snapshot-the-manager) for resource *stacks* — a
`FileToIdConverter.json` over the tag directory, listed with
`FileToIdConverter.listMatchingResourceStacks` — so every copy of
*tags/block/logs.json* across the enabled packs is visited in priority
order and merged, and a *replace* in a higher pack discards what the lower
packs contributed to that id. A pack whose file fails to parse is
logged and skipped, never fatal.

### Tags of tags resolve in order, and a tag with a hole is dropped whole

The vanilla *logs* file names no block at all: it is three
tag references, *logs_that_burn*, *crimson_stems* and *warped_stems*, and
*logs_that_burn* is in turn ten references, *oak_logs* among them, before
*oak_logs* finally lists four blocks. `TagLoader.build` feeds every tag
reference into a `DependencySorter` and resolves the leaves first. A tag
with **any** failing entry — a missing required element as much as a
missing required tag reference — is dropped whole, not loaded minus the
entry, and is then absent from `Registry.getTags`, so neither the network
payload nor a lookup will find it. An optional entry (*required: false*)
resolves to nothing and the tag still builds: `TagEntry.build` answers *not
required* on a miss, whichever lookup it was handed. What the two lookups
differ on is **where a required id is looked up**, and the difference decides
what a missing one costs. `TagLoader.ElementLookup.fromFrozenRegistry`, used
for a registry that has already frozen, asks it either way: a name it does
not hold is a miss, and the tag is dropped.
`TagLoader.ElementLookup.fromGetters`, used by data-pack registries as they
load, sends a required id through the *registration* lookup, which
manufactures a placeholder `Holder.Reference` for a name nothing has
registered (`MappedRegistry.createRegistrationLookup`), and an optional one
through the immutable lookup, which does not. A data-pack registry has
registered all its elements before it reads its tags, so a placeholder there
is never bound: the registry's freeze fails, with unbound values, and takes
the whole load with it.

### Prepared, then applied

This is the swap the opening promised, and it is three steps and no lock.
`MappedRegistry.prepareTagReload` refuses a registry that is not frozen and
builds the new table, reusing the freeze's `HolderSet.Named` objects where it
can; nothing is installed yet, and the `Registry.PendingTags.lookup` it hands
back answers with the new table's names, which is what the worldgen and
reloadable-registry codecs are given while they load. `Registry.PendingTags.apply` is
then **three ordered steps**: bind each `HolderSet.Named`, swap the
`MappedRegistry.TagSet`, then rebind every holder's tag set in
`MappedRegistry.refreshTagsInHolders`. Each step replaces an immutable
collection with one reference write, so a reader on another thread —
worldgen on the worker pool during a `/reload`, the Render thread in
singleplayer — sees a holder's old tags or its new ones and never a
half-built set; what it can see, for the length of the rebind, is some
holders already new and others still old. Which thread applies depends on the
occasion: at world load the Server thread does not
exist yet, so the apply runs on whichever thread is driving the load — the
launching thread on a dedicated server, through `Util.blockUntilDone`, and
the Render thread on the client. Only a reload of a running server — `/reload`, or `/datapack enable` or `/datapack disable` — applies on the Server thread.

### The client gets integers

`TagNetworkSerialization.serializeTagsToNetwork` walks
`RegistrySynchronization.networkSafeRegistries` and writes each tag as a
list of registry ids, dropping any registry whose payload came out empty.
That set is *every* `RegistryLayer.STATIC` registry, unconditionally,
concatenated with the synced dynamic ones —
`RegistrySynchronization.isNetworkable` filters only the second group. Ids
for dynamic registries are meaningful only once both sides have built the
same registry in the same order, which is why `SynchronizeRegistriesTask`
sends the registry data first. In the configuration phase that ordering is
a *send*-order constraint rather than a handling one: the client buffers
both packets and resolves everything at the end. The play-phase packet
resolves immediately against the live registry access, and the client then
rebuilds the creative-inventory search tree from the new tags.

### Singleplayer skips only what it already has

On the play path
`ClientPacketListener.handleUpdateTags` always prepares, and skips only the
*apply* on a memory connection, because the integrated server's apply
already rebound the registries both halves share. In configuration the
suppression is narrower still: only the non-networkable (static)
registries' tags are skipped, and the client still binds tags on its own
copies of the remote dynamic registries, copies it then discards for the
server's own ([identifiers and
registries](identifiers-and-registries#when-a-world-opens)).

### The check is a field read

`BlockBehaviour.BlockStateBase` is a
`TypedInstance`; `TypedInstance.is` asks the type holder — for a block,
`Block.builtInRegistryHolder`, the intrusive holder from
[identifiers-and-registries](identifiers-and-registries#before-the-game-exists) — and
`Holder.Reference.is` is set-contains on an interned `TagKey`. No registry
is consulted. `Parrot` (the perch search), `TrunkPlacer` (worldgen) and the
client's `PunchTreeTutorialStepInstance` all ask this way; `FluidState`,
`Entity` and `ItemStack` go through the same interface.

## The other way tags cross the wire

`ClientboundUpdateTagsPacket` is not the only one. `ByteBufCodecs.holderSet`
encodes a `HolderSet.Named` as a marker plus the tag's `Identifier`, and
decodes it by looking the tag up in the receiving side's registry — so any
packet or data component carrying a tag-shaped `HolderSet` **hard-fails on
a client that does not have that tag**. That, more than the id numbering,
is why the tags packet must reach the client before play traffic does.

The same idea appears in data: `TagKey.hashedCodec`, `HolderSetCodec` and
`RegistryCodecs` are what turn *"#minecraft:logs"* in an ordinary JSON field
— a recipe ingredient, a loot condition, a placement predicate — into a
`HolderSet` without any of those files being tag files.

## Questions players ask

**What if two tags reference each other?** Both are dropped, and both are
logged. `DependencySorter.addDependencyIfNotCyclic` drops the edge that
would close the cycle, so an order exists and the sort does not hang — but
whichever tag is built first asks for a tag that is not in the new table
yet, gets nothing, and fails as a missing required reference; the second
then asks for the first, which failed, and fails the same way. Two
*Couldn't load tag* lines, and neither tag exists.

**What happens to a tag a `/reload` deleted?** It keeps its old contents,
and nothing tells you. The tag is absent from the pending map, so apply
neither rebinds nor clears it: anything still holding that `HolderSet.Named`
will *iterate* the old list, while `HolderSet.Named.contains` — which reads
the holder's refreshed tag set — answers false, and `Registry.get` for the
key answers empty.

**Is picking a random element from a tag deterministic?** Yes, per pack
stack. Duplicate entries collapse and file order is preserved —
`TagLoader.tryBuildTag` collects into an insertion-ordered set — so
iterating a tag, or picking from it with `HolderSet.getRandomElement`,
gives the same sequence for the same packs.

**Which tags does a client never hear about?** Those of
`RegistryLayer.RELOADABLE`-layer registries (loot tables, predicates) and of
non-synced worldgen registries (features, structures) — and on
the receiving side, ids the client's own registry does not know are dropped
from the payload silently.

> **For a 1.21-era reader.** There is no *TagManager* and no reload listener
> for registry tags: loading is static functions on `TagLoader`, called from
> `WorldLoader`, `MinecraftServer.reloadResources`, `ReloadableServerRegistries`,
> the registry load tasks and the client's `RegistryDataCollector`, and only
> function tags still go through a real listener. The directory is singular and has no plural fallback —
> *tags/block*, not *tags/blocks* — because `Registries.tagsDirPath` builds
> that string in one place and accepts nothing else.

## Where to look

`TagKey` · `BlockTags` · `TagFile` · `TagLoader` · `Registry.PendingTags` ·
`MappedRegistry` (the tag half) · `HolderSet.Named` ·
`ReloadableServerResources` · `TagNetworkSerialization` ·
`Parrot.ParrotWanderGoal`

---

*Rules: names, never code · how the system works, not how the code reads ·
newest version only · every backticked name passes `tools/verify_names.py`.*
