token.setProperty()
Write a property on a token, resolving the name the same way
getProperty() does.
Part of the Action API · Function reference
Available on token, and — with caveats — on target and targets.
Signature
token.setProperty(propertyName: string, value: any): void
Synchronous, returns nothing.
Parameters
| Parameter | Type | Description |
|---|---|---|
propertyName |
string |
Name, alias, or id. Case-insensitive, resolved as in getProperty(). |
value |
any |
The new value. Any JSON-serializable value. |
Returns
Nothing. There is no success or failure signal — read the value back if you need to be sure.
Which key gets written
The point of setProperty() is that it writes the key the property already
uses, rather than inventing a parallel one:
- If the token already stores the property under any name, alias, id, or differently-cased spelling, that existing key is updated.
- If the property is defined by the system or campaign but not yet stored on this token, it is created under the canonical name from the definition.
- If nothing is known about the name at all, it is created exactly as you spelled it.
That last case is how you end up with hitpoints sitting next to hp — if a
property matters, define it in campaign settings first.
This is how you write to a token
Direct assignment does not work:
token.name = 'Bob'; // ignored — core fields are protected
token.setProperty('hp', 12); // this is the way
Assigning to a core field (name, id, position, and the rest) is silently
refused. Only setProperty() and the writable metadata bag change anything.
Persistence
A write through setProperty() on the acting token is saved to the server
after the action finishes. You do not save anything yourself.
Writes to a target are a different story — see the warning below.
The combat log
Every change is recorded in the combat log as a property change, with the old and new values. A write of the value it already had is skipped, so no-op writes do not clutter the log.
Hit-point changes made by applyDamage() do not pass through
here — they are logged as damage instead, so nothing is double-counted.
Examples
Spend a resource
const slots = token.getProperty('spellSlots') ?? 0;
if (slots <= 0) return `${token.name} has no spell slots left.`;
token.setProperty('spellSlots', slots - 1);
Toggle a flag
token.setProperty('concentrating', true);
Track something structured
const conditions = token.getProperty('conditions') ?? [];
token.setProperty('conditions', [...conditions, 'prone']);
Heal without an approval card
// Direct write — no GM approval, no damage modifiers, no card.
// Use applyDamage(-n) if you want the usual healing flow instead.
const hp = token.getProperty('hp') ?? 0;
const max = token.getProperty('hp_max') ?? hp;
token.setProperty('hp', Math.min(hp + 5, max));
Notes and gotchas
- Writing to a target is possible, and probably a mistake.
targetblocks direct field assignment but does not blocksetProperty()— the write lands on the local copy and is not saved, so it disappears the next time that token refreshes. Treat targets as read-only and useapplyDamage(), which is the sanctioned way to change someone else's token. - No validation. Writing
'twelve'into a numeric property stores the string, and laterroll()expressions using that property will quietly drop the term. - The Test panel writes for real in memory but does not persist, so a value you set while testing reverts.
- Nothing is announced.
setProperty()sends no chat message; combine it withsendMessage()if the table should know.
See also
getProperty()applyDamage()— the approved way to change another token- Chat and the combat log