Body, Mind & Soul — API Reference
    Preparing search index...

    Workflows — Step-by-Step GM Task Guides


    When: A player takes a hit, you're adjudicating damage, or running a script effect.

    1. Find the actor ID if you don't have it:

      search_documents(query: "Character Name", documentTypes: ["Actor"])
      
    2. Apply damage via run_javascript:

      const actor = game.actors.get("ACTOR_ID");
      return await actor.applyDamage(AMOUNT, "TYPE", "tempHp");
      • This respects resistances, cascades through layers (tempHp → shields → health), fires hooks, and posts a chat message automatically.
      • Returns { original, actual } — actual may be lower due to resistances.
    3. If you want to preview first without applying:

      return game.actors.get("ACTOR_ID").system.calculateDamage(AMOUNT, "TYPE", "tempHp");
      

    1. run_javascript:
      const actor = game.actors.get("ACTOR_ID");
      await actor.applyRestore("health", { useSurge: true, consumeSurge: true });
      • Restores floor(health.max / 4) + any bonusValue, deducts one surge.
      • For shields or sanity, change "health" to "shields" or "sanity".

    When: You need to set a specific value without surge logic (e.g., "set HP to 5", "set sanity to 0").

    1. Read the actor first (required for update):

      read_document(documentType: "Actor", documentId: "ACTOR_ID")
      
    2. Update the specific field:

      update_document(
      documentType: "Actor",
      documentId: "ACTOR_ID",
      updates: { "system.health.value": 5 }
      )

    1. run_javascript with the tracker summary from common-scripts.
    2. Or: read_document(documentType: "Combat", documentId: "COMBAT_ID") and look at system.bms.downs.

    To find the active combat ID: run_javascriptreturn 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).

    1. Check whether this is the cause:
      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.
    2. If non-zero, resolve just those pending actions (no decrement/sweep/tick/round bookkeeping — safe to call as many times as needed):
      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.
    3. If a specific action is genuinely broken and 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.
    4. Once 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.

    1. 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 };
    2. 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:

    1. 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" }]
      }
      }]
      )

    1. 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";
    2. If you need to see the full schema:

      read_document(documentType: "Actor", documentId: "EXISTING_NPC_ID")
      
    3. Create:

      • Use create_document with documentType: "Actor" and include type: "npc" in the data.
      • Set vitals, attributes, and resistances in system.*.
      • Add items (actions, features) via embedded operations or separate create_document calls.

    1. 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 }));
    2. If any linked effects exist, confirm with the GM whether to disable them.

    3. 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:

    • Conditions array should be [] or use alwaysNEVER use isThisItem (it doesn't exist and silently blocks the trigger)
    • chatMessage content uses {actor.name} tokens, NOT @actor
    • amount in outcomes must be a string (e.g. "1" not 1)
    • NEVER use dot-path array indexing like system.triggers.0.hook — always update via the array field
    list_documents(documentType: "Item")
    read_document(documentType: "Item", documentId: "EXISTING_ITEM_ID")

    Include 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.


    1. Search journal entries:

      search_documents(query: "keyword", documentTypes: ["JournalEntry"])
      
    2. 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.

    1. In the actors directory, click "Create Actor".
    2. Select type: Party.
    3. Name it (e.g., "Party Group").
    4. Save.
    1. Open the Party sheet (GM-only).
    2. Click "Add Member".
    3. Select an actor from the dialog.
    4. Repeat for each member.
    1. Open the Party sheet.
    2. Click "Push Effect to All Members" (top button).
    3. Select a stackable effect or regular effect from the dropdown.
    4. Enter the initial stack count (e.g., 1 for a buff that starts at 1 stack; ignored for non-stackable effects).
    5. Click "Push".
      • The party creates one ActiveEffect record directly on the Party actor for each member.
      • Each member's actor receives a thin partyEffectLink pointer item that dereferences to their own party-held record.
      • The party-held record is stamped with that member's UUID so the Party sheet can group effects by member.
      • Each member sees the effect in their own allApplicableEffects list via the pointer item.

    Within each member's collapsible section in the Party sheet:

    • Raise (+) button: increase that member's stack count by 1.
    • Lower (−) button: decrease by 1 (cannot go below 0; effect goes dormant at 0 stacks instead of disappearing).
    • Numbers shown as (current/max), e.g., 3/5.
    • Changes are applied only to that member's copy — other members' stacks remain independent.

    A member can opt out of a party-sourced effect from their own character sheet:

    1. Open the member's character sheet.
    2. Find the party-sourced effect in the Effects tab (marked with a mint-green border, "Party" source).
    3. Click the delete button (X).
      • This deletes only that member's pointer item.
      • The party-held ActiveEffect record remains on the Party actor for other members and the GM.
      • Other members continue to see and use the effect independently.
    1. Open the Party sheet.
    2. In a member's effects list, find the party-sourced effect.
    3. Click the up-arrow button (Remove from All Members).
    4. Confirm in the dialog.
      • All members' party-held ActiveEffect records are deleted from the Party actor.
      • All members' pointer items are deleted from their actors.
      • The effect is completely gone from the party and all members.
    1. From the Party sheet, click the edit button (pencil icon) on the effect.
      • Opens the Party's copy of the ActiveEffect for editing.
      • Changes affect only this Party copy (for this specific member in their independent stack count, etc.).
    2. To make an edit reach all members at once (e.g., changing name, description, or triggering changes):
      • Edit the original source of the push (the journal page, feature effect, or action effect the GM originally pushed from).
      • All members' copies automatically update via Foundry's origin-forwarding mechanism.

    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.