Repository navigation
global token actor identity
Audience: anyone in the suite resolving, comparing, naming, or writing to an actor.
Practical knowledge about Foundry v13 unlinked tokens. Verified core-only.
Read this before writing any line that identifies a creature. Three modules got the same thing wrong within two days -- Blacksmith, Bibliosoph and Squire, independently, in code that had worked for months. Once each looked, it was twelve sites: three in Blacksmith, two in Bibliosoph, eight in Squire. One was reported by a user; the other eleven were found only by knowing what to grep for.
It is not an obscure edge. It is what happens the first time a GM fields more than one of
something, which is the first session of most campaigns. And it survives review because
currentActor?.id === actor?.id looks exactly like an identity check.
Resolve by token to identify or apply. By actor only to aggregate.
actor.idis an aggregate identity.actor.uuidis a specific one.
Comparing with .id asks "the same KIND of creature?". Comparing with .uuid asks "the
same creature?". Most code means the second and writes the first.
The ordinary way to field a group is to drop one monster on the canvas and copy-paste it -- nineteen cultists from one actor. Every copy is unlinked: it carries its own ActorDelta, so it has its own hit points, its own effects, and its own name on the nameplate. Foundry builds a synthetic actor per token from base + delta, and that object is genuinely distinct.
But it reports the base actor's id:
token.document.actorLink false
token.actor.uuid Scene.rSvj….Token.7s08….Actor.k2wg… <- distinct per token
token.actor.uuid Scene.rSvj….Token.Acwj….Actor.k2wg… <- distinct per token
token.actor.id k2wgXXD2aQU5co4F <- SHARED by all copies
token.actor.name "Bandit" <- the PROTOTYPE's name, shared by all copies
token.name "Tusk" / "Patch" <- what the GM sees and means
A delta does not override name. This is the detail that catches people: the tokens are
visibly named, the actor behind each is genuinely separate, and actor.name still returns the
prototype's name for all of them.
| You want | Use | Not |
|---|---|---|
| Is this the same creature? |
actor.uuid, or token.id
|
actor.id |
| Apply damage, an effect, a condition | the token's actor -- canvas.tokens.get(tokenId)?.actor, or fromUuid(targetUuid)
|
game.actors.get(actorId) |
| Name it for a human | token.name |
actor.name |
| Its current hit points, AC, effects | the token's actor | the base actor |
| Count kills per monster type, aggregate statistics |
actor.id -- correct here |
|
Compare against a directory actor -- user.character, an assigned PC, anything that is always linked |
actor.id -- correct here, and .uuid would be noise |
Do not sweep .id to .uuid blindly. The second row above is real: a user's assigned
character is always a directory-linked actor, so .id is exact for it and changing it gains
nothing while making the comparison harder to read. The question is never "is this .id" but
"can the thing on either side of this ever be an unlinked token's actor?"
Resolve token-first with the actor as fallback, never the reverse:
const actor = canvas?.tokens?.get(data.tokenId ?? '')?.actor
?? game.actors.get(data.actorId ?? '');fromUuid() on a Scene.X.Token.Y.Actor.Z uuid returns that token's own delta actor, which
is what anything applied should be written to.
None of these throws. All six shipped.
| Symptom | Cause |
|---|---|
| Every hit and MVP line names the same creature, so damage spread across a group reads as damage landing repeatedly on one | named from actor.name
|
| A card shows a damage number that does not match the token it is about | percentage computed from the prototype's max HP |
| A toast names a creature that was not involved |
actor.getActiveTokens()[0] -- an arbitrary copy |
| Selecting a different copy does nothing; the panel refuses to switch |
currentActor?.id === actor?.id reading "unchanged" |
| Two copies fight over one window; opening the second shows the first's data | an application id built from actor.id
|
| A heal or damage aimed at one copy lands on all of them | a selectable row keyed on actor.id
|
The first one is the dangerous one, and not because it is the worst bug. It is the one that makes a GM report something else entirely. A display that calls nineteen creatures by one name looks exactly like damage being applied to all of them at once, and the resulting hunt goes looking for a data bug that does not exist. Confirm what actually moved before believing what it looked like.
A diagnostic worth keeping, from the Squire instance: two independent entry points failed
together -- selecting a token on the canvas, and clicking one in the Party tab. Neither entry
point was broken. The Party tab already keyed on token.id and selected the right token; both
paths then met one .id comparison downstream. When two independent entry points fail the
same way, the shared thing downstream is the candidate, and fixing either entry point is
wasted work.
grep -rn "\.id === .*\.id\|actor\?\.id ===" scripts/ # comparisons
grep -rn "game\.actors\.get(" scripts/ # resolution
grep -rnE '\$\{[a-zA-Z.?]*[Aa]ctor\??\.id\}' scripts/ # IDENTITY STRINGS
The third one matters and was added late. The first two find comparisons and lookups; they
do not find an actor.id baked into a string that then BECOMES an identity -- an application
id, a row key, a dataset attribute, a socket payload. Two of the twelve sites were that shape,
including the only one that applied damage, and neither of the first two greps would have
reached them.
For each hit, ask: identify, apply, or aggregate? The first two are wrong here and the third is right, and the answer is usually obvious once the question is asked. That it is obvious in review and invisible while writing is exactly why this page exists.
A quick live check, with two copies of one monster on the canvas:
canvas.tokens.placeables
.filter(t => t.document.actorId === '<base actor id>')
.map(t => ({ name: t.name, uuid: t.actor?.uuid, id: t.actor?.id,
hp: t.actor?.system?.attributes?.hp?.value }));Distinct uuid, identical id, and name that differs from actor.name -- that is the
shape everything above follows from.
- Ai assistance
- Applicationv2 traps
- CSS traps
- dnd5e conditions
- Documentation standard
- Token actor identity
- Campaign
- Canvas
- Chatcards
- Compendiums
- Contextmenu
- Core
- Create journal entry
- Dialog
- Active Effects
- Entity list
- Equiplocations
- Geography
- GM request
- Gmnotes
- Health
- Hookmanager
- Importer
- Inventory
- Menubar
- Notes
- Party
- Pins
- Quantity split
- Requestroll
- Rolls
- Scene config
- Sockets
- Stats
- Tags
- Toast
- Tokens
- Toolbar
- Window
- Worldclock