Skip to content

token.requestRoll()

Ask the player who owns a token to make a roll, and wait for the number.

Part of the Action API · Function reference

Available on token, target, and every entry of targets. It is almost always called on a target — a saving throw or an opposed check.

Signature

token.requestRoll(expression: string, options?: {
  label?: string;
  text?: string;
  pipeToChat?: boolean;
}): Promise<number | null>

Asynchronous — await it.

Parameters

Parameter Type Default Description
expression string — The dice expression to roll. Property names resolve against that token's stats, not the caster's. Same grammar as roll().
options.label string none Shown in the owner's prompt and as a header on the resulting chat row, e.g. 'Dex save'. Always set this — an unlabelled roll request is a mystery.
options.text string none Chat template for the result. {result} is replaced with the rendered roll.
options.pipeToChat boolean true false rolls silently on the owner's client — no chat row, no 3D dice. You still get the total.

Returns

Promise<number | null>:

  • a number — the total the owner rolled
  • null — they declined, or never answered

null is not 0. Handle it explicitly; treating a decline as a rolled zero turns "I'd rather not" into a failed save.

const save = await target.requestRoll('1d20+dex', { label: 'Dex save' });
if (save === null) return;

What the other player sees

The request goes over the socket to the owner of the token it was called on. They get a prompt naming the token and the label, with a roll and a decline option. The roll executes on their client, with their 3D dice, against that token's stats — so a player still rolls their own saves.

The result lands in chat inside your action's frame, so the exchange reads as one event rather than two unrelated messages.

Examples

A saving throw

const dc = 15;
const damage = roll('8d6', { label: 'Fireball' });
const save = await target.requestRoll('1d20+dex', { label: 'Dex save' });
if (save === null) return;
target.applyDamage(save >= dc ? Math.floor(damage / 2) : damage, 'fire');

Every target saves in turn

for (const t of targets) {
  const save = await t.requestRoll('1d20+wis', { label: `Wis save (DC ${dc})` });
  if (save === null) continue;
  if (save < dc) t.applyDamage(roll('4d6'), 'psychic');
}

An opposed check

const mine = roll('1d20+athletics', { label: 'Grapple' });
const theirs = await target.requestRoll('1d20+athletics', { label: 'Resist grapple' });
if (theirs === null) return;
sendMessage(theirs >= mine
  ? `${target.name} breaks free.`
  : `${token.name} has ${target.name} pinned.`);

Custom result text

await target.requestRoll('1d20+con', {
  label: 'Con save',
  text: `${target.name} resists the poison with {result}.`,
});

Notes and gotchas

  • Your action stops until they answer. Everything after the await waits on another human. Do the parts that do not depend on the answer first.
  • A loop over targets is sequential. Each await waits for that player before the next prompt goes out. For a big area effect, consider whether the GM would rather roll them.
  • Expressions resolve against the rolling token. '1d20+dex' uses the target's dexterity — which is the whole point, but it means a property that exists on the caster and not the target quietly drops out of the total.
  • The token needs an owner. A token nobody owns has nobody to prompt, and the request resolves null.
  • The Test panel really sends it. Unlike chat messages, roll requests reach the other player during a test run.
  • Unhandled null is the most common bug in action scripts. Check it every time.

See also

  • roll() — rolling for the acting token
  • applyDamage() — what usually follows a failed save
  • prompt() — asking the clicking user something