Skip to content

prompt()

Ask the person who clicked the action a question, and wait for the answer.

Part of the Action API · Function reference

Signature

prompt(message: string, jsType?: NumberConstructor | BooleanConstructor | StringConstructor): Promise<any>

prompt() is asynchronous — always await it. Without await you get a promise object, not an answer.

Parameters

Parameter Type Default Description
message string — The question. If it contains an <input>, <select>, or <textarea> the dialog switches to form mode.
jsType Number | Boolean | String String Simple mode only. How to interpret what was typed. The constructors and their lowercase string names ('number', 'boolean', 'string') are both accepted.

Returns

Promise resolving to:

Mode Resolves to
Simple, no jsType or String The raw string
Simple, Number A number
Simple, Boolean true
Form An object keyed by each field's name
Cancelled (Escape, Cancel, or the dialog dismissed) null

Always handle null. Cancelling is a normal thing for a player to do.

const n = await prompt('How many targets?', Number);
if (n === null) return;

Simple mode

One question, one input, an OK and a Cancel.

const name = await prompt('Name the summoned creature');
const bonus = await prompt('Circumstance bonus?', Number);

Number fields accept arithmetic, not just digits: a player can type 32-14, 3*4, or (5-1)*4 and the answer resolves to the computed number. Only + - * / and parentheses are supported. Text that is neither a number nor a valid expression is rejected in the dialog, and the player is asked again — the promise stays pending until they submit something valid or cancel.

Boolean is best avoided for now. true, yes, and 1 resolve to true, but false, no, and 0 are currently rejected as invalid input rather than resolving to false. Use a <select> in form mode, or ask for a string and compare it yourself.

Form mode

If the message contains form markup, the whole string is rendered as a form and the answer is an object.

const answers = await prompt(`
  <label>Damage type
    <select name="type">
      <option value="fire">Fire</option>
      <option value="cold">Cold</option>
    </select>
  </label>
  <label>Bonus <input name="bonus" type="number" value="0"></label>
  <label><input name="empowered" type="checkbox"> Empowered</label>
`);
if (answers === null) return;
// { type: 'fire', bonus: 0, empowered: false }

Every field needs a name — that is the key in the returned object. A field without one is dropped.

Values are coerced by field type:

Field Value in the result
<input type="number"> or type="range" number, arithmetic allowed (32-14)
<input type="checkbox"> boolean — unchecked fields are included as false
<select multiple> string[] of the selected values
Everything else string

Native validation applies: required, min, max, and pattern block submission with the browser's own message.

The HTML is sanitized before rendering. Allowed tags are the form controls (input, select, option, textarea, fieldset, legend, label) plus basic text markup (div, span, p, br, hr, strong, em, b, i, small, h1–h6, ul, ol, li, details, summary). Everything else — <script>, <style>, <button>, <a>, and every on* event attribute — is stripped. Add data-autofocus to the field that should have the cursor.

Examples

Spend a resource, then act

const spend = await prompt(`
  <label>Spell slot level
    <select name="level" data-autofocus>
      <option value="1">1st</option>
      <option value="2">2nd</option>
      <option value="3">3rd</option>
    </select>
  </label>
`);
if (spend === null) return;
const level = Number(spend.level);
const damage = roll(`${2 + level}d6`, { label: `Burning Hands (${level})` });
for (const t of targets) t.applyDamage(damage, 'fire');

Build a picker from the campaign's damage types

const options = damageTypes.map((d) => `<option value="${d.id}">${d.name}</option>`).join('');
const answer = await prompt(`
  <label>Damage <input name="amount" type="number" data-autofocus></label>
  <label>Type <select name="type">${options}</select></label>
`);
if (answer === null) return;
target.applyDamage(answer.amount, answer.type);

Notes and gotchas

  • One prompt at a time. Opening a second prompt while one is still on screen replaces it, and the first promise never resolves. await each one before asking the next.
  • The clicking user answers, not the target's owner. To ask a different player something, use requestRoll().
  • The action is paused while the dialog is open. Anything you did before the await — messages, rolls, property writes — has already happened and will stand if the player cancels. Ask first, act after.
  • Cancel resolves null; it does not throw. Code after an unchecked await prompt(...) keeps running with null in hand.
  • Escape and Enter. Escape cancels. In simple mode Enter submits; in form mode it does not, so multi-line and multi-field forms behave normally.

See also