When: A player takes a hit, you're adjudicating damage, or running a script effect.
Find the actor ID if you don't have it:
search_documents(query: "Character Name", documentTypes: ["Actor"])
Apply damage via run_javascript:
const actor = game.actors.get("ACTOR_ID");
return await actor.applyDamage(AMOUNT, "TYPE", "tempHp");
{ original, actual } — actual may be lower due to resistances.If you want to preview first without applying:
return game.actors.get("ACTOR_ID").system.calculateDamage(AMOUNT, "TYPE", "tempHp");
run_javascript:const actor = game.actors.get("ACTOR_ID");
await actor.applyRestore("health", { useSurge: true, consumeSurge: true });
When: You need to set a specific value without surge logic (e.g., "set HP to 5", "set sanity to 0").
Read the actor first (required for update):
read_document(documentType: "Actor", documentId: "ACTOR_ID")
Update the specific field:
update_document(
documentType: "Actor",
documentId: "ACTOR_ID",
updates: { "system.health.value": 5 }
)
run_javascript with the tracker summary from common-scripts.read_document(documentType: "Combat", documentId: "COMBAT_ID") and look at system.bms.downs.To find the active combat ID: run_javascript → return game.combat?.id
When: The GM says "advance/next down isn't doing anything" or "the tracker seems stuck."
advanceTracker()/advanceToNextResolution() refuse to run (silent no-op, console warning only) whenever down 0 has actions that are due (resolutionTime <= 0) but not yet resolved, or a resolution is genuinely in progress right now (e.g. a player's target-selection prompt is still open) even if the due-action list momentarily reads empty — most commonly after the GM's browser got interrupted mid-resolve, or while the GM is simply waiting on a player to pick targets. This is intentional: advancing past an unresolved due action used to be able to silently lose it from the tracker, and clicking advance a second time while a resolution was genuinely pending used to be able to cancel that pending prompt and cause its effects to apply twice or not at all (fixed v0.7.2 follow-up).
return game.combat?.system.getDueUnresolvedActions().length ?? 0;
Note this only reports the durable due-but-unclaimed case. If it reads 0 but the tracker still shows "Resolve pending actions" instead of a plain advance arrow, a resolution is actively mid-flight (blocked on a prompt) — usually just tell the GM to wait for the player to respond, since clicking advance again is safe (it's refused cleanly, not destructively) but won't help. If the prompt is genuinely hung (a network stall, or the GM's own picker never completing) rather than just awaiting a normal response, see step 3 below — "Mark Resolved" now force-cancels that specific in-flight prompt too, not just genuinely broken/erroring actions.await game.combat.resolveCurrentDown();
The tracker UI does this automatically: its advance button swaps to a "Resolve pending actions" play icon whenever this condition (or the in-flight-resolution one above) is true.resolveCurrentDown() can't get past it (e.g. it keeps erroring), OR if it's genuinely hung on a prompt that will never be answered (a stalled connection, or the GM's own interactive picker never completing), the GM can force it resolved from the tracker's right-click context menu ("Mark Resolved," behind a confirmation dialog) or via script:await game.combat.markActionResolved("ACTION_ID");
This skips the action's actual resolve effects (no damage/triggers/prompts) — confirm with the GM before using it, it's a last resort. If the action is currently in progress, this also force-cancels its specific in-flight prompt before marking it resolved (leaving every other action's prompt untouched); if it's claimed by something other than a cancellable prompt, it reports an explicit error instead of doing nothing.getDueUnresolvedActions().length is back to 0 and no resolution is genuinely in flight, advanceTracker()/advanceToNextResolution() work normally again.When: You need to add an action to the tracker on behalf of a combatant.
Find the actor and item:
const actor = game.actors.getName("CHARACTER NAME");
const item = actor.items.getName("ACTION NAME");
return { actorId: actor.id, itemId: item.id, swings: item.system.swings };
Queue it:
await game.combat.queueActionFromSheet(
game.actors.get("ACTOR_ID"),
"ACTION_ITEM_ID",
0 // swing index (0 = first swing)
);
When: Casting a spell, applying a condition, or tracking something that expires after N downs.
const combat = game.combat;
const durationDowns = 4; // 4 downs = 2 ticks = 2 seconds
await combat.addEffectToDown(durationDowns, {
name: "Blinded",
description: "Cannot see",
icon: "icons/svg/blind.svg",
combatantId: "COMBATANT_ID", // or null for unlinked
activeEffectId: null, // or a real ActiveEffect ID to auto-disable on expiry
resolutionTime: durationDowns,
originalResolutionTime: durationDowns,
});
Duration reference: 2 downs = 1 tick (1 second), 12 downs = 1 round (6 seconds).
When: A spell or ability grants a status effect.
Option A — Enable an existing (disabled) effect:
const actor = game.actors.get("ACTOR_ID");
const ae = actor.effects.getName("EFFECT NAME");
await ae.update({ disabled: false });
Option B — Create a new effect:
read_document the actor first, then:update_document(
documentType: "Actor",
documentId: "ACTOR_ID",
operations: [{
type: "insert",
collection: "effects",
data: {
name: "Burning",
img: "icons/svg/fire.svg",
disabled: false,
changes: [{ key: "system.resistances.fire", mode: 2, value: "0.5" }]
}
}]
)
run_javascript to inspect an existing NPC for reference schema:
const npc = game.actors.find(a => a.type === "npc");
return npc ? { id: npc.id, name: npc.name } : "No NPCs found";
If you need to see the full schema:
read_document(documentType: "Actor", documentId: "EXISTING_NPC_ID")
Create:
create_document with documentType: "Actor" and include type: "npc" in the data.system.*.create_document calls.Check for lingering tracker effects with linked ActiveEffects:
// down.effects is an id-keyed object (TypedObjectField), not an array.
const c = game.combat;
return c.system.bms.downs.flatMap(d => Object.values(d.effects).filter(e => e.activeEffectId))
.map(e => ({ name: e.name, actor: e.actor?.name }));
If any linked effects exist, confirm with the GM whether to disable them.
End combat:
await game.combat.endCombat();
(This will prompt the GM in-client to disable linked effects.)
When: Creating a consumable or activatable item that fires a BMS trigger when used.
IMPORTANT RULES — read before attempting:
[] or use always — NEVER use isThisItem (it doesn't exist and silently blocks the trigger){actor.name} tokens, NOT @actoramount in outcomes must be a string (e.g. "1" not 1)system.triggers.0.hook — always update via the array fieldlist_documents(documentType: "Item")
read_document(documentType: "Item", documentId: "EXISTING_ITEM_ID")
create_document callInclude ALL trigger data inline. No follow-up patching needed.
{
"documentType": "Item",
"data": {
"name": "Surge Restorer",
"type": "item",
"img": "icons/svg/item-bag.svg",
"system": {
"description": "Restores 1 surge when used.",
"quantity": 1,
"usable": true,
"triggers": [
{
"id": "",
"hook": "bms.itemUsed",
"enabled": true,
"conditions": [],
"outcomes": [
{
"id": "",
"type": "changeResource",
"config": { "resource": "surges", "operation": "add", "amount": "1" }
},
{
"id": "",
"type": "chatMessage",
"config": { "content": "{actor.name} restores 1 surge." }
}
]
}
]
}
}
}
read_document(documentType: "Item", documentId: "CREATED_ITEM_ID")
Confirm triggers[0].hook is correct and outcomes contains the right entries.
system.triggers supports partial updates — include only the fields you want to change, identified by id. Nested conditions and outcomes are also merged by id.
Change just a trigger's hook (all other fields preserved):
{ "updates": { "system.triggers": [{ "id": "triggerId", "hook": "bms.combatDown" }] } }
Update one outcome's config (other outcomes preserved):
{
"updates": {
"system.triggers": [{
"id": "triggerId",
"outcomes": [{ "id": "outcomeId", "config": { "content": "New message." } }]
}]
}
}
Full replacement (omit id from all items — same behaviour as before):
{ "updates": { "system.triggers": [{ "hook": "bms.itemUsed", "enabled": true, "conditions": [], "outcomes": [...] }] } }
If you need to read_document first to get the current trigger IDs, do so — but you only need to send back the fields you're changing.
Search journal entries:
search_documents(query: "keyword", documentTypes: ["JournalEntry"])
Read the result:
read_document(documentType: "JournalEntry", documentId: "JOURNAL_ID")
Then look through pages for the relevant page content.
const actor = game.actors.get("ACTOR_ID");
return {
id: actor.id,
name: actor.name,
type: actor.type,
items: actor.items.map(i => ({ id: i.id, name: i.name, type: i.type })),
effects: actor.effects.contents.map(e => ({ id: e.id, name: e.name, disabled: e.disabled })),
resources: Object.keys(actor.system.resources),
actionGroups: actor.system.actionGroups?.map(g => g.label) ?? [],
};
When: You want to apply an effect to all party members at once, or manage party-sourced effects.
partyEffectLink pointer item that dereferences to their own party-held record.allApplicableEffects list via the pointer item.Within each member's collapsible section in the Party sheet:
3/5.A member can opt out of a party-sourced effect from their own character sheet:
Note: Party-sourced effects show as "Party" source and mint-green styling on each member's own character sheet's effects tab as well — you can toggle/edit/delete them there too, but the Party sheet view is the best place for bulk operations and per-member stack adjustments.