Action API
The JavaScript available inside an action script.
An action is a string of JavaScript that runs when someone clicks the button. It runs in the clicking user's browser, with a fixed set of values and functions already in scope — you do not import anything.
The editor has autocomplete for everything on this page, and a Test panel that runs your code without sending anything to chat.
Every function has its own page in the function reference — full signatures, every parameter and return value, edge cases, and worked examples. This page is the tour; that is where the detail lives.
What is in scope
| Name | What it is |
|---|---|
token |
The acting token — the one being impersonated |
character |
The character behind the token, read-only, or null |
target |
The single targeted token, or null |
targets |
All targeted tokens, as an array |
roll |
Roll dice, with the result shown in chat |
rollSilent |
Roll dice, with nothing said in chat |
rollDetailed |
Roll dice without touching chat, with the individual dice |
sendMessage |
Write to chat |
prompt |
Ask the clicking user a question |
damageTypes |
The campaign's damage types |
draw |
Draw shapes on the map, where available |
area |
The area the player placed while targeting, or null |
api |
The namespace new surface lands in: api.pickSquares, api.importCharacter, api.importParty, api.importRelatedCharacters, api.chat.stream, api.chat.brief, and api.version |
Your campaign's campaign functions are also in scope.
Action code is async, so you can await at the top level.
Dice
roll('1d20+5') // rolls, shows 3D dice + a chat row, returns the total
rollSilent('2d6+3') // rolls, says nothing, returns the total
roll('1d20+strength') // property names work in expressions
roll('1d20', { label: 'Perception' })// a labelled chat row
roll returns the total immediately. await is optional.
Use rollSilent when the roll is not the message —
a spell that rolls its damage, applies it and then says one sentence about the
whole thing, rather than leaving four raw dice rows above that sentence.
(roll(expr, false) still does the same; the named form is easier to find.)
const r = rollDetailed('2d6+3');
// { total, rolls: [4, 2], expression, breakdown }
Use rollDetailed when you want the individual dice, or to format the result
yourself.
Chat
sendMessage(`${token.name} swings wide and misses.`);
sendMessage('<b>Critical hit!</b>');
HTML is supported. Dice expressions inside the text are rendered as dice.
Every message an action sends is grouped into one chat entry, so a five-message action does not spam the log.
The acting token
Read any property:
token.name
token.metadata.hp
token.getProperty('strength') // alias- and case-insensitive
token.characterId // the character behind it, or null
Write through the API rather than assigning directly:
token.setProperty('hp', 12); // works, and logs to the Combat Log
token.name = 'Bob'; // does NOT work — core fields are protected
Other methods on token:
| Method | Does |
|---|---|
getProperty(name) |
Read a property, resolving aliases |
setProperty(name, value) |
Write a property |
getDistanceTo(tokenOrId) |
Distance in grid units |
getLineOfSightTo(tokenOrId) |
Whether sight is clear, using the map's walls |
applyDamage(amount, type?) |
Deal damage (negative heals) |
requestRoll(expr, opts?) |
Ask this token's owner to roll |
addAction(name, code) |
Add another action to the token |
changeTokenArtwork(index) |
Switch map artwork |
changePortraitArtwork(index) |
Switch portrait |
autoTokenizeArtwork() |
Re-crop the current art as a token |
Targets
target and targets behave like token, but you cannot assign to their
fields. They can still take damage and be asked to roll:
if (target) {
target.applyDamage(roll('2d6'), 'fire');
}
for (const t of targets) {
t.applyDamage(roll('1d6'), 'cold');
}
Targeting is enabled per action; if the action does not target, target is
null and targets is empty.
When an action targets an area, the region the player placed is also in
scope as area — pass it to draw() to leave the zone on
the map exactly where they aimed it. It is null for single-target actions and
for actions run outside targeting mode, so check before using it.
Summoning
An action can put characters on the map (Action API 0.2, v0.86.0). Each of these asks the user for one square per character and creates a token there, exactly as the map's Import Character does:
// One character, by id or slug.
const owl = await api.importCharacter('archimedes');
// A whole party, by id or name.
const skulls = await api.importParty('The Skulls');
// The characters related to this one on its sheet — Shelly's minions.
const skeletons = await api.importRelatedCharacters(token.characterId, ['minion']);
All three resolve to null if the user cancels the square pick, so check
before using the result. Relationships are set on the character sheet, under
Campaign & Party in Build mode. api.pickSquares()
is the building block, if you want squares for something else.
The source character
character is the sheet behind the token, read-only, and null when there is no
sheet — monsters and scenery usually have none, so guard every use.
if (!character) return `${token.name} has no character sheet.`;
const level = character.getProperty('level');
| Method | Does |
|---|---|
getProperty(name) |
Read an attribute (computed), metadata, or an exposed field |
getCurrentTokenArtworkId() |
Index of the artwork used as map art |
getCurrentPortraitArtworkId() |
Index of the artwork used as the portrait |
Read token for the state of the figure on the map, and character for what the
sheet says — they can legitimately disagree.
Damage and healing
target.applyDamage(12, 'slashing'); // damage
token.applyDamage(-5); // heal 5
Damage is not applied immediately. It appears in chat as a card showing the calculation — including the target's resistances, vulnerabilities, and immunities — which the GM or the token's owner approves, modifies, or denies. Hit points change on approval.
This is deliberate: an action can propose damage, but it cannot silently rewrite someone else's character.
Asking a question
const n = await prompt('How many targets?', Number);
if (n === null) return; // the user cancelled
Number fields accept arithmetic, so a user can type 32-14.
For several fields at once, pass HTML containing form controls; the result is an
object keyed by each field's name:
const answers = await prompt(`
<label>Damage type <select name="type">
<option>fire</option><option>cold</option>
</select></label>
<label>Bonus <input name="bonus" type="number" value="0"></label>
`);
// { type: 'fire', bonus: 0 }
Asking another player to roll
const save = await target.requestRoll('1d20+dex', { label: 'Dex save' });
if (save !== null && save < 15) {
target.applyDamage(roll('8d6'), 'fire');
}
The target's owner gets a prompt on their own screen and rolls their own dice.
Resolves to null if they decline or never answer — always handle that case.
A complete example
// Fireball
const dc = 15;
sendMessage(`${token.name} hurls a fireball!`);
const damage = roll('8d6', { label: 'Fireball' });
for (const t of targets) {
const save = await t.requestRoll('1d20+dex', { label: 'Dex save' });
if (save === null) continue;
const taken = save >= dc ? Math.floor(damage / 2) : damage;
t.applyDamage(taken, 'fire');
}
Returning a value
If your action returns a string, it is sent to chat. So this is a complete action:
return `${token.name} looks around suspiciously.`;
Limits and gotchas
- Core token fields are read-only. Use
setPropertyfor anything you want to change; direct assignment totoken.nameand friends silently does nothing. - Damage always goes through approval from a script. There is no way to bypass it.
drawis not always present. Check it exists before using it.- Rolls are synchronous.
roll()returns a number, not a promise;awaiton it is harmless but unnecessary.promptandrequestRollare promises and must be awaited. - A
requestRollthat is declined resolves tonull, not to 0. - Errors surface in the action's chat output. Use the Test panel while developing.
Full reference
Every function above has a page of its own, with all its parameters, return values, failure modes, and examples:
Globals — roll() ·
rollSilent() ·
rollDetailed() ·
sendMessage() ·
prompt() ·
draw()
Token methods — getProperty() ·
setProperty() ·
applyDamage() ·
requestRoll() ·
getDistanceTo() ·
getLineOfSightTo() ·
addAction() ·
changeTokenArtwork() ·
changePortraitArtwork() ·
autoTokenizeArtwork()
Character methods —
character.getProperty() ·
getCurrentTokenArtworkId() ·
getCurrentPortraitArtworkId()
api.* — api.pickSquares() ·
api.importCharacter() ·
api.importParty() ·
api.importRelatedCharacters() ·
api.history.commits() ·
api.history.diff() ·
api.campaign.configuration() ·
api.treasury ·
api.timeline
Safety
Action code runs in a restricted environment: it cannot reach arbitrary websites, read your files, or escape the page. It can change your campaign's data through the API above.
Only run actions from people you trust — the same standard you would apply to a spreadsheet macro someone sent you.