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.
Booleanis best avoided for now.true,yes, and1resolve totrue, butfalse,no, and0are currently rejected as invalid input rather than resolving tofalse. 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.
awaiteach 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 uncheckedawait prompt(...)keeps running withnullin 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
requestRoll()— ask another player for a rollapplyDamage()— where a damage-type answer usually goes- Action API overview — including
damageTypes