Skip to content

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 setProperty for anything you want to change; direct assignment to token.name and friends silently does nothing.
  • Damage always goes through approval from a script. There is no way to bypass it.
  • draw is not always present. Check it exists before using it.
  • Rolls are synchronous. roll() returns a number, not a promise; await on it is harmless but unnecessary. prompt and requestRoll are promises and must be awaited.
  • A requestRoll that is declined resolves to null, 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

→ Function reference index

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.