Skip to content

How the Automation System Works

Back to API Reference · Feature guide: Automation Engine

For anyone writing activations, in the Activation Manager UI or through the registration API. Covers what the engine does between a game event and your activationCode running.

For trigger payload schemas, see API Reference. For API surfaces (effects, bonuses, interactive tools), see the sibling files: Effects API, Interactive API, Combat API, Items API.

Note

This started as a reaction reminder, so the internals are named after reactions: you write reactions: [...], the action path field is reactionPath, and you register with api.registerDefaultItemReactions() / api.registerDefaultGeneralReactions(). Those all mean activation. Legacy naming, not a separate concept.



Table of Contents



1. Big Picture

The automation system is an event-driven dispatcher. Each game event (a move, an attack, a damage roll, a status applied, a turn change, etc.) becomes a trigger with a data payload. For each trigger, the engine asks: which tokens on the scene want to react, and how?

Two kinds of definitions can react:

  • Item activations: tied to a Lancer item by its LID. Only tokens whose actor owns that item are candidate reactors.

  • General activations: not tied to any item. Any token in the scene is a candidate, filtered by your rules.

Each definition can declare:

  • One or more triggers it cares about.

  • Filters (disposition, distance, self/other, in/out of combat).

  • An evaluate function that returns true/false (one final synchronous check).

  • An activationCode function (your effect) that runs when the activation fires.

  • Optional onInit (runs once when a token enters the scene/combat) and onMessage (cross-client request).

Vocabulary

The names used by every callback and the rest of this doc:

Name Meaning
trigger a game event turned into a dispatch: a trigger type plus a data payload
triggerData the payload your callbacks receive. Enriched per reactor, carries the helpers and cancel functions
triggeringToken the token whose action fired the trigger
reactor / reactorToken the token being checked for a reaction, the one reacting (nothing to do with the mech part)
item the reactor's matched item. null for general activations
activationName the name of the action at the action path, else the item's name, else the general activation's name
LID Lancer ID, item.system.lid. Stable identifier, same across all copies of an item
flow the Lancer system's step pipeline for one action (attack, damage, activation). Every chat card is the output of a flow



2. Lifecycle of a Trigger

Flow diagram (click to expand)
flowchart TD
    A["Game event<br/>(move, hit, damage, status, turn...)"] --> B["handleTrigger(triggerType, data)"]
    B --> C["Iterate all tokens on the scene"]
    C --> D["Compute distanceToTrigger, canTriggerReaction<br/>and merge into data"]
    D --> E{"Item reactions<br/>matched by LID?"}
    E -- No --> G
    E -- Yes --> F["Run filters + evaluate()"]
    F --> G{"General reactions<br/>matched by name<br/>or unconditional?"}
    G --> H["Run filters + evaluate()"]
    H --> I{"autoActivate?"}
    I -- Yes --> J["activateReaction()<br/>now, or after the sweep<br/>on cancellable triggers"]
    I -- No --> K["Push to reactionQueue"]
    K --> L["After all tokens processed,<br/>show summary popup<br/>via socket"]
    L --> M["User clicks Activate"]
    M --> J
    J --> N["activationType +<br/>activationMode dispatch"]
    N --> O["Your activationCode runs<br/>(or flow / macro / chat card)"]

What the engine does for one trigger:

  1. Trigger fan-out. A flow step or hook calls handleTrigger(triggerType, data), or your code calls api.dispatchCustomTrigger(name, data) (Custom triggers). The engine wires the reaction helpers (startRelatedFlow, startRelatedFlowToReactor, sendMessageToReactor, debugActivation) onto triggerData. Signatures in section 8.

  2. Reactor sweep. Every token on the scene is a potential reactor. When the triggering token is hidden, only the reactors that are the mover itself or one of its targets are considered. Everyone else is skipped.

  3. Distance enrichment. For each reactor, two values are computed once and merged into a per-reactor copy of the trigger data:

    • distanceToTrigger - distance from the reactor to the triggering token.
    • canTriggerReaction - whether the trigger may provoke a reaction. It's false when the mover has hidden, disengage, or the provoke immunity, or is intangible while the reactor is not.
  4. Item reactions first. For every item the reactor's actor owns whose LID matches a registered item activation, run the filter chain.

  5. General reactions second. Walk the flat list of general activations that listen to this trigger, and run the filter chain.

  6. Filter chain (any failure = skip). Order and details in section 4.

  7. evaluate() runs synchronously (see section 5). Exceptions are caught and logged, and the activation is skipped on error.

  8. Branch on autoActivate:

    • true: activateReaction() runs on the local client.
    • false: the entry is pushed to reactionQueue.

    On the seven cancellable triggers (onPreMove, onPreStructure, onPreStress, onPreStatusApplied, onPreStatusRemoved, onPreHpChange, onPreHeatChange) auto activations are not run inline. They are collected and run after the whole sweep, triggering token first, stopping at the first cancel. That ordering is why Engagement beats Overwatch.

  9. Summary popup. After every token is processed, if the queue is non-empty, the engine builds a summary popup, decides who sees it (per the reactionNotificationMode setting), and broadcasts it via socket.

  10. Manual activation. When a recipient clicks Activate on a queued entry, that client runs activateReaction() for that single entry.

  11. Post-activation sweep, on onActivation and onEndActivation only. Once the awaited auto activations have finished, the engine fires onPostActivation / onPostEndActivation with the same payload plus results, what each reaction returned (see activationCode). Popup activations resolve later and are not in it.

Filters and evaluate run for every reactor on the scene, every time a matching trigger fires. Keep them cheap.

Custom triggers

Any name that is not built in. api.dispatchCustomTrigger(name, data) enters at step 1. Listen via the editor's Custom field or triggers: ["myTrigger"]. Always fires, outOfCombat does not apply. data becomes triggerData, so pass triggeringToken for the token filters. Built-in names and onInit* are refused. Not cancellable, no consumption.



3. Item vs General Activations

Picking one

If you want to... Use
React when a specific weapon, system, or NPC feature is used Item, with onlyOnSourceMatch: true
React when any hostile starts moving in your threat General, no source match
Apply a passive effect at scene-load to anyone with a feature Item, onInit only (no triggers)
Build a one-off rule that applies to all tokens General
React on a specific deployable's action (or its deploy) Deployable LID (actor.system.lid), with onlyOnSourceMatch: true
React only as one specific actor or deployable instance Actor UUID (actor.uuid) in the LID field, with onlyOnSourceMatch: true
React once per event as the scene, not per token General with sceneReactor: "add" or "only", sceneId to limit it to one scene
React when you deploy something yourself Item LID that grants the deployable, triggers: ["onDeploy"], triggerSelf: true

Item activation

  • Registered under a specific item LID (e.g. "npcf_dispersal_shield_priest").

  • Engine checks: does this reactor's actor own an item whose system.lid === <registered LID>? If yes, run filters.

  • The matched item is passed to your callbacks as the item argument.

  • onlyOnSourceMatch: true means: the activation only fires if the item that triggered the event has the same LID as the activation's LID. Without it, your activation would also try to fire when the user activates an unrelated item or moves.

  • When a reactor owns several items sharing the triggering LID, only the exact triggering document fires (same-LID dedupe), so duplicate copies of the same item don't each react.

General activation

  • Registered under a name (e.g. "Overwatch", "Brace").

  • Every token on the scene is a candidate. Use filters to narrow it down (disposition, distance, your own evaluate).

  • The item argument to your callbacks is null (general activations are not tied to an item).

  • onlyOnSourceMatch: true means: the activation only fires if the triggering action's name matches the activation's registered name. Useful for general activations attached to a named action like Overwatch.

Deployable activation

  • Registered under a deployable LID, the value of actor.system.lid on the deployable actor (e.g. "dep_moonlight_drone"). The engine matches any LID against the deployable's actor LID - there is no required prefix.

  • The reactor is the deployable actor itself: any deployable on the scene whose actor.system.lid === <registered LID> is a candidate.

  • The engine auto-resolves the source item (the item whose system.deployables[] contains the deployable's LID - for frames, also core_system.deployables and traits[].deployables) and surfaces it as triggerData.item, so authors can read effect/tag context.

  • triggerData.deployable = { actor, lid } is also set, providing the explicit deployable identity.

  • triggerData.actionData.action.name is the action's name (e.g. "Move", "Combine"). For the top-level deploy click it's the deployable actor's name. triggerData.actionData.action.activation is the activation type ("Protocol", "Quick").

  • onlyOnSourceMatch: true means: only fire when the activation source is the matching deployable - i.e. one of this exact deployable's actions was the trigger.

  • reactionPath selects which part of the deployable a reaction binds to:

    • empty string - the top-level deploy itself (actor.system.activation).
    • "actions.<name>" - a specific entry in actor.system.actions[] (mirrors the extraActions.<name> pattern).

The deployable surrogate carries the deployable actor's system.actions[], so these resolve by name (deployable actions often have empty LIDs).

Self-deployable

The onDeploy trigger fires when a deployable or a thrown weapon is placed. Its payload is { triggeringToken, item, deployedTokens, deployType }:

  • triggeringToken - the deploying token.
  • item - the source system item.
  • deployedTokens - the freshly placed tokens.
  • deployType - "deployable" or "throw".

To react when you deploy something, register an item activation on the feature or system LID that grants the deployable, and set:

  • triggers: ["onDeploy"]

  • triggerSelf: true (the deploying token is the reactor, so it counts as self)

  • onlyOnSourceMatch: true (only fire for this exact source item)

The Restock Drone NPC feature (startups/itemActivations.js) is the reference: on deploy it reads triggerData.deployedTokens[0] and drops an aura on it with api.createAura(...). This is a normal item reaction firing on self, not the actor-UUID path below.

Actor UUID activation

Alongside item LIDs and deployable LIDs, an activation can be registered against a single Actor UUID (e.g. "Actor.qe5wEevLrMN6ki44", or a "Compendium.<pack>.Actor.<id>" path). The engine synthesizes an actor_surrogate for every actor, keyed by its actor.uuid, so a reaction keyed by a UUID matches only that one actor instance.

Use this when an item or deployable LID is too broad: a LID-keyed reaction fires for every actor that owns the item or every deployable of that type, while a UUID-keyed reaction is bound to exactly one actor. Typical case: a specific named deployable or NPC that should react only as itself.

  • onlyOnSourceMatch: true also compares the triggering token's actor.uuid against the registered key (alongside item and deployable LIDs), so a UUID reaction with source-match only fires when that actor was the source of the event.

  • In the Activation Manager, paste the Actor UUID into the LID field, or use the Actor Finder to fill it in.

  • A world actor's UUID (Actor.<id>, the prototype) matches every token spawned from it, linked or not. An unlinked token's own actor.uuid (Scene.<id>.Token.<id>.Actor.<id>) matches that one placed token only.



4. Filters, in Order

Filters short-circuit, in this order (rows 6 and 7 apply to item activations only):

# Filter Behavior
1 onlyOnSourceMatch Matches the triggering item LID, deployable LID, or actor UUID against the registered key. Meaning per kind in section 3.
2 outOfCombat If combat is not active and outOfCombat is false, skip. Unless the trigger is inherently combat-related (onTurnStart, onTurnEnd, onRoundStart, onEnterCombat, onExitCombat) or is a custom trigger.
3 triggerSelf / triggerOther / triggerTarget If the reactor is the triggering token: require triggerSelf: true. If it isn't: pass with triggerOther: true, or with triggerTarget: true when the reactor is one of the event's targets. triggerOther defaults to true: it only skips when you set it to exactly false.
4 checkReaction Skip the reaction when the reactor has no reaction left this round. Runs only when the field is set true. Spending is separate: the world setting consumeReaction.
5 requireCanProvoke If true, skip if triggerData.canTriggerReaction is false.
6 item availability A destroyed or disabled item never reacts, same for an action path that no longer resolves.
7 checkUsage Skip when the item is unloaded, out of uses, uncharged, or past its per-round / per-turn tag limit. Same gate as the editor's Check Usage box, which is on unless you clear it. A config registered from code that omits the field gets no gate at all.
8 dispositionFilter Array like ["hostile", "friendly"]. Uses Token Factions multi-team data when installed, otherwise CONST.TOKEN_DISPOSITIONS.
9 evaluate() Your custom predicate. Last gate. Range checks go here, compare triggerData.distanceToTrigger.

Fail any: that activation is skipped for that reactor, with no popup. Three cases still speak up: an onActivation dropped only by outOfCombat warns once, a checkUsage on an item that has no usage tag to check warns once, and the Debug: Out of Combat Warnings setting warns on every out-of-combat drop. Debug mode (Automation Engine - Debugging) logs which filter dropped it.

The three identities. An actor does a thing, everyone may react:

Gate Passes when the reactor is...
triggerSelf the one doing it
triggerTarget one of the event's targets
triggerOther anyone else. It still admits targets, so an older config written before triggerTarget existed keeps working. Defaults to on
  • "Target only" is Self off, Other off, Target on.
  • triggerTarget only works on triggers whose payload carries targets (attacks, tech, damage, onRoll, onCheck, onInvoluntaryMove). The editor greys the rest.
  • Target-reactors get isTarget: true and targetEntry (their own roll/crit on hit/miss triggers), and they still fire when the attacker is hidden. Being attacked is knowable.
  • A target-side reaction that cancels (cancelAttack, cancelDamage) must be autoActivate: manual popups routed to another client receive serialized data without the cancel functions.



5. The Four Callbacks

All four receive api as the last argument. All four are wrapped in try/catch. Uncaught exceptions are logged to the console, never thrown to the user. Argument names are defined in the vocabulary.

evaluate

evaluate(triggerType, triggerData, reactorToken, item, activationName, api) => boolean

The final filter. Return true to allow the activation, false to skip it.

Must be synchronous, on every trigger. If evaluate returns a Promise the engine logs a console.error and treats the result as false, so the activation is dropped.

activationCode

activationCode(triggerType, triggerData, reactorToken, item, activationName, api) => Promise<any>

Your effect. May be async. Has full access to api. Runs on:

  • The local client (auto activations).

  • Whichever client clicks Activate in the popup (manual activations).

See section 8 for what that means for GM-only operations.

Return value. Optional, and normally nothing. On onActivation and onEndActivation, whatever the code returns is collected into results[activationName] and handed to the next trigger, onPostActivation / onPostEndActivation. That is the handoff for one automation extending what another just did, without re-registering it: the first returns what it produced, the second reads it.

Stock Bolster returns { targets, effects }, the tokens it bolstered and the effects it placed on them. A talent that lengthens Bolster reads them back:

triggers: ["onPostActivation"],
triggerSelf: true,
triggerOther: false,
evaluate: (triggerType, triggerData) => triggerData.actionName === "Bolster",
activationCode: async (triggerType, triggerData, reactorToken, item, activationName, api) => {
    for (const effect of triggerData.results?.Bolster?.effects ?? [])
        await api.setLAFlag(effect, 'duration', { label: 'indefinite' });
}

Only awaited auto activations are collected. Reactions with awaitActivationCompletion: false and popup activations are not, they resolve too late.

onInit

onInit(token, item, api) => Promise<void>

Runs once when a token (or its item) enters the scene. Used for:

For a scene reactor (sceneReactor on) it also runs on scene load, see sceneReactor.

onInit is not a trigger. Do not put "onInit" in the triggers array. The engine looks for the onInit field on the reaction config and calls it directly when:

  • A token is created on the canvas (after a 100 ms delay so the canvas object exists), or

  • An item is added to an actor that already has a token on the scene.

For reactions that only have an onInit and no triggers, set triggers: [], triggerSelf: false, triggerOther: false, autoActivate: false, activationType: "none".

onMessage

onMessage(triggerType, data, reactorToken, item, activationName, api) => Promise<any>

Handler for cross-client requests. Invoked when another client calls triggerData.sendMessageToReactor(data, userId, opts). Runs on the target client (the user named by userId).

If the caller passed wait: true, whatever you return (or resolve(...)) from onMessage is delivered back as the caller's return value. See Interactive API for the wait card / response pattern.



6. Activation Type and Mode

Two independent dimensions on each reaction config:

activationType

Value Effect
"code" Run your activationCode function. The most common choice.
"flow" Launch the reaction's own flow: the reactionPath action, else the item's first Reaction action, else its first action, else the system flow, else a generic chat card (weapons get this, and it never rolls an attack). General reactions post a trigger/effect card.
"macro" Execute a Foundry macro by name (activationMacro field).
"none" Do nothing. Typically only used for onInit-only reactions.

activationMode

Macro/code only. Ignored for "flow" and "none".

Value Effect
"instead" Your code runs alone. Default for item reactions.
"after" The reaction's own flow/card fires alongside your code. Default for general reactions.

activationMode never touches the flow you are reacting to. That one runs regardless. And "after" is not ordered: both run together via Promise.all. For strict ordering use "instead" and call the flow yourself, or inject into the flow state with injectBonus / injectFlowExtraData (see section 11).

sceneReactor

General activations only. Default "off".

Value Effect
"off" Per-token evaluation as usual.
"add" Also evaluated once as the active scene, on top of the per-token passes.
"only" Evaluated once as the active scene, never per token. onInit is the exception: it still runs per token.

The scene pass:

  • runs on the GM client only, before the token passes.
  • reactorToken is a scene stand-in: isSceneReactor: true, .scene, .name, .document.texture.src, actor: null.
  • token gates do not apply (triggerSelf / triggerOther / triggerTarget, checkReaction, dispositionFilter, requireCanProvoke). outOfCombat and evaluate do.
  • triggerData carries isSceneReactor: true and scene.
  • use activationType: "code" or "none", a flow needs an actor.
  • with onlyOnSourceMatch it still fires once as the scene, on the action whose name matches.

sceneId (the Scene select in the editor) limits the whole activation, token passes included, to one scene. Empty means every scene.

onInit on a scene reactor also runs when a matching scene loads (canvasReady, or once the module is ready on first load), GM only, with the scene stand-in as token and item null. Use it for "when this map opens, set up X".



7. Auto Mode vs Popup Mode

Auto

The activation runs immediately, on the local client, with no UI. Use this for things that should always happen:

  • Applying a status on hit.

  • Adding/removing a constant bonus.

  • A passive that should fire silently.

The activation is queued. After every reactor has been checked, all queued entries for the trigger are bundled into a single summary popup. Each entry shows the reactor's name and the activation's label. Clicking an entry expands its details, Activate runs that entry's activationCode.

Warning

Only one activation popup exists at a time. If a second trigger raises its own popup, the previous one is closed and its unclicked entries are gone. There is no queue and no pending badge. Anything that must not be missed belongs on autoActivate.

Who sees the popup

Controlled by Module Settings > Activation Notification Mode:

Setting Recipients
"both" (default) The token's owner(s) and the active GM
"owner" Only the token's owner(s)
"gm" Only the active GM

The popup is broadcast over the socket only to the recipients. Other clients see nothing.



8. Clients and Sockets

activationCode does not always run on the GM's client.

  • Auto activations run on whichever client called handleTrigger: the client of the player whose action fired the trigger (e.g. who moved the token), or the GM if the triggering token has no online owner.

  • Manual activations run on whichever client clicked Activate. Per the reactionNotificationMode setting, that may be the token owner, the GM, or either: first to click wins.

If your activationCode needs GM-only permissions (creating tokens, modifying actors the local user doesn't own, deleting templates owned by someone else, etc.), you must either:

  1. Use an API helper that already delegates internally (most applyEffectsToTokens, placeDeployable, placeZone, addGlobalBonus calls do this).

  2. Call triggerData.sendMessageToReactor(data, gmUserId, { wait: true, ... }) to delegate to the GM and wait for the response.

A common shortcut: api.getActiveGMId() returns the user ID of the active GM, suitable for sendMessageToReactor.

Reaction helpers

Wired onto triggerData for every reaction. Call from evaluate / activationCode.

startRelatedFlow() async - runs the reacting item's own default flow on the current client (weapon → WeaponAttackFlow, action/reactionPath → ActivationFlow, system → SystemFlow, else a simple activation card).

startRelatedFlowToReactor(userId = null, extraData = {}, opts = {}) async - same, but on userId's client (null → reactor's owner). extraData is merged into the flow and reads back as triggerData.extraData on its onActivation:

triggerData.startRelatedFlowToReactor(userId, { chargeSpent: 2 });

opts: { wait, waitTitle, waitDescription, waitItem, waitOriginToken, waitRelatedToken }. wait:true awaits remote completion. The wait* fields fill the local "waiting" card. extraData must be JSON-serializable. See the True Grit example.

sendMessageToReactor(data, userId = null, opts = {}) async → any - RPC to the reactor's onMessage (same opts). With wait:true, returns its result. Delegation primitive for GM-only work.

debugActivation(label?) - logs triggerType, triggerData, reactorToken, item, activationName to the console and returns the same as an object. Also on the api as api.debugActivation(triggerType, triggerData, reactorToken, item, activationName, label?). Debug mode and breakpoints: Automation Engine - Debugging an automation.



9. Cancel and Modify

A subset of triggers fire before the underlying action commits. From inside evaluate or activationCode, you can stop or modify the action.

The synchronous rule

The cancel/modify functions only work synchronously. The operative rule is one line: call the cancel or modify function before your first await. Everything after an await reaches the flow too late. An async activationCode is fine as long as the cancel call happens in its synchronous head, which is exactly the preConfirm / postChoice pattern below.

What awaitActivationCompletion actually does

It is not the escape hatch it looks like.

  • The default already awaits. The engine awaits an auto activation unless the flag is set to exactly false. Leaving it unset awaits.

  • On the seven cancellable triggers it does nothing. onPreMove, onPreStructure, onPreStress, onPreStatusApplied, onPreStatusRemoved, onPreHpChange and onPreHeatChange defer their auto activations to the end of the sweep and invoke them without awaiting, so the flag is never read. This is the case the flag looks like it should fix, and it is the one case it cannot.

  • Its one real effect is on activations written as code strings in the editor. An async string that mentions a cancel/modify function, or sits on a timing-sensitive trigger, raises a permanent ui.notifications.warn telling you it will probably fail to block. Setting the flag suppresses that warning. A config registered from code with real functions never hits it.

Cancel functions

Signature: (reasonText?, title?, allowConfirm?, userIdControl?, preConfirm?, postChoice?, opts?). A Cancel/Ignore card is shown by default. cancelTriggeredMove omits the title slot.

Trigger Cancel function Effect
onPreMove cancel() Stops the move with no card and no reason text
onPreMove cancelTriggeredMove Stops the move outright
onPreMove changeTriggeredMove(newPos, extraData?, reason?, allowConfirm?, ...) Redirects the move to a new destination
onInitAttack cancelAttack Aborts the attack flow
onInitTechAttack cancelTechAttack Aborts the tech attack flow
onInitCheck cancelCheck Aborts the stat check flow
onInitActivation cancelAction Aborts the activation flow
onInitEndActivation cancelAction Suppresses the end card, the item is already inactive
onPreStatusApplied / onPreStatusRemoved cancelChange Blocks the status change
onPreStructure cancelStructure Skips the structure roll
onStructure cancelStructureOutcome Stops the outcome step (after the roll)
onPreStress cancelStress Skips the overheat roll
onStress cancelStressOutcome Stops the outcome step (after the roll)
onPreHpChange cancelHpChange Blocks the HP change
onPreHeatChange cancelHeatChange Blocks the heat change
onInvoluntaryMove cancel(reason) Blocks the forced move

Modify functions

They block the original update and commit the replacement value instead. modifyHpChange and modifyHeatChange take (newValue, reason?, allowConfirm?, userIdControl?, preConfirm?, postChoice?, opts?). Note the missing title: they drop that slot, unlike the cancels. Check the per-trigger signature below before passing positional arguments.

Trigger Function Effect
onPreHpChange modifyHpChange(newValue, ...) Override the HP value being applied
onPreHeatChange modifyHeatChange(newValue, ...) Override the heat value being applied
onStructure / onStress modifyRoll(newTotal) Override the roll total before outcome steps
onRoll changeRoll(newTotal, reason?, title?, allowConfirm?, userIdControl?) Override any roll total. This one does keep title

onStructure / onStress also expose triggerData.rollResult (total) and triggerData.rollDice (raw die results, useful for detecting double-1s or doubles).

Why preConfirm and postChoice exist

Foundry's preUpdate* hooks (move, actor update, etc.) are non-blocking: if your handler returns a Promise, Foundry does not wait for it. Anything after an await happens too late to stop the update.

So the pattern is: 1. Call the cancel/modify function synchronously (before any await). This preemptively blocks the update.

  1. Afterwards, do your async work (choice cards, remote player decisions, etc.) to decide what happens next.

preConfirm and postChoice are the two async hooks that let you drive that "what happens next" phase without needing to pre-build the UI yourself.

allowConfirm, preConfirm, postChoice

Shared across every cancel, change, modify, and reroll function.

allowConfirm: boolean (default true). Whether to show the secondary Confirm/Ignore card to userIdControl.

  • true: show the card, user can override.
  • false: skip the card, auto-pick the first choice (the "no override" outcome).

Does NOT gate preConfirm. preConfirm always runs if provided.

preConfirm: () => Promise<boolean>. Async gate that runs after the sync cancel has stuck, before the secondary card.

  • Returns true: proceed to the secondary card (or auto-pick if allowConfirm: false).
  • Returns false: re-apply the original outcome. postChoice does NOT fire.

Typical use: the reactor's player confirms Yes/No in a choice card here. Good place to call triggerData.startRelatedFlowToReactor(...) so the reactor's own flow fires once on commit.

postChoice: (chose: boolean) => Promise<void>. Callback after the secondary card resolves (or after auto-pick).

  • chose === true: the replacement was committed.
  • chose === false: the original was re-applied (user picked Ignore).

Typical use: fire a downstream effect that depends on whether the action went through.

Timing

sync:  setFlag()       -> engine blocks the original update
async: preConfirm()    -> false: executeOriginal + return (no postChoice)
       allowConfirm?
         true  -> show card
                  Confirm: executeNew + postChoice(true)
                  Ignore:  executeOriginal + postChoice(false)
         false -> executeNew + postChoice(true)

Example: True Grit

const preConfirm = async () => {
    const ask = await api.askCard({
        title: "TRUE GRIT",
        description: `<b>${ally.name}</b> would fall to 0 HP. Keep at 1 HP?`,
        owner: reactorToken
    });
    if (ask.confirmed)
        triggerData.startRelatedFlowToReactor(ask.responderIds[0]);
    return ask.confirmed;
};
triggerData.modifyHpChange(
    1,
    `${reactorToken.name} keeps ${ally.name} at 1 HP.`,
    true,
    api.getTokenOwnerUserId(ally),
    preConfirm,
    null
);



10. Economy and Frequency

The Lancer reaction economy (1 reaction per round) has two separate parts: the reaction config's checkReaction filters out a reactor with no reaction left before evaluate runs (only when set true), and the world setting consumeReaction, when enabled, spends one reaction each time a Reaction-type action fires.

Other frequency-related fields:

  • actionType: seven values, "Automation" (the default), "Reaction", "Free Action", "Quick Action", "Full Action", "Protocol", "Other". It labels the popup entry, but it is not only a label: it is written into the flow data as the activation, which is what consumeReaction reads. Set it to "Reaction" and the activation can spend the token's reaction.

  • frequency: display string ("1/Round", "1/Combat") shown on the popup entry. Never enforced.

  • outOfCombat: opt-in for triggers that wouldn't normally fire outside combat. Not needed for custom triggers.

Real limits come from two places. Item tags (tg_round, tg_turn, limited uses, loading, recharge) are enforced by the checkUsage filter. Everything else uses the gate API: await api.consumeOncePerRound(reactorToken, 'my_key', target) is true the first time this round per target, and consumeGate covers longer windows. See Flags API. The Triangulation Ping NPC feature is the worked example.



11. Flow Data Injection

Inside an activationCode whose trigger carries a flowState (most attack/damage/check/activation triggers), you can mutate the in-progress flow. Three methods are wired onto the state:

triggerData.flowState.injectBonus({
    id: "my-bonus",
    name: "Marker Rifle Mark",
    type: "accuracy",
    val: 1
});

triggerData.flowState.injectFlowExtraData({ myFlag: true });

const extra = triggerData.flowState.getFlowExtraData();

Lifetime. injectFlowExtraData values live on flowState.la_extraData for the rest of the flow. They're also serialized onto the resulting chat message, so a damage card produced later in the same flow can still read what was injected during the attack step. injectBonus is different: a bonus is dropped from the flow's bonus list the moment it is actually applied, so it lands on one roll and not the next.

Use this when you need a bonus to apply exactly to this one roll/attack/damage card without leaving a global bonus or a status effect on the actor.



12. Paths and Extra Actions

Three related mechanisms for binding reactions to sub-parts of an item and for tracking whether an item is currently "on".

Action path

An activation config can target a specific sub-action of an item instead of the item as a whole. The field is stored as reactionPath on disk (legacy name from when the system only handled reactions). In the UI and in this doc it is called action path.

Common forms:

  • "actions[0]" - the first action on a regular item

  • "extraActions.Fall Prone" - an extra action stored on the item via addExtraActions

  • "ranks[2]" - rank-3 of a talent

  • "profiles[0]" - a weapon profile, fires when switched to

Warning

The path is relative to item.system, so it carries no system. prefix. "system.actions.0" resolves item.system.system, which is undefined, and the activation quietly never fires. Only the flow launcher tolerates the prefix, which is why a config can look half-working. The Find Action browser always emits the correct form.

At evaluation time the engine walks the path into item.system (or the extraActions flag), pulls the name off the action found there, and uses it as activationName. If onlyOnSourceMatch is set, the activation only fires when the triggering action's name matches the one at the path.

This is how one item can have multiple independent activations (different sub-actions, different talent ranks) without colliding.

Finding the right path. The item finder inside the Activation Manager lists every action on the selected item next to its action path - no need to guess the index or dig through the item's schema.

Extra actions

API: addExtraActions, getItemActions, getActorActions, removeExtraActions (all on InteractiveAPI, documented in HUD API).

Adds action objects (name, activation type, description, tags, etc.) onto an item, token, or actor via flags. Two storage locations: - Passed an item: stored in the item's extraActions flag. Merged into the item's action list by getItemActions.

  • Passed a token or actor: stored on the actor. Returned by getActorActions.

Typical uses: Sniper's Mark adds a "Fall Prone" action, Limitless adds "Overcharge (NPC)", and Defense Net adds "Collapse the Defense Net" while it's active.

activation field must use TAH's short form ("Quick", "Full", "Protocol", "Free", "Reaction", "Quick Tech", "Full Tech"). Not "Quick Action" etc. TAH filters by strict equality.

Binding an activation to an extra action. Two ways:

  • By general name - register a general activation whose name matches the action name. Source matching is done on the activation name directly. This is the only option for an action injected onto a token or actor, and it also works for an item-stored one (Limitless binds its item-stored "Overcharge (NPC)" this way).

  • By action path - when the extra action is injected onto an item, register an item activation with reactionPath: "extraActions.<Name>". The engine resolves the sub-action through the flag lookup the same way it walks actions[N].

Granting to someone else. An action added to another actor needs opts.grant on addExtraActions, which marks it with the token and item it came from. It is deleted when that token or item is deleted. A condition on the action gates it the rest of the time, returning hidden, locked or disabled. Both in HUD API - Granted actions. The Rotary Grenade Launcher in startups/itemActivations.js is the worked example: an aura hands adjacent allies a Quick action that reloads the owner's weapon.

Access. Extra actions are currently only surfaced through the Lancer Automations TAH. Other UIs (the native Lancer sheet, the native action bar, etc.) do not show them. If the TAH is disabled, extra actions are invisible to the user even though they still fire when triggered from code.

Activated items lifecycle

For items that stay "on" after being activated (auras, persistent effects, stances), the engine tracks an activated state per token.

How the three combine

The end action raises onEndActivation instead of onActivation (and onInitEndActivation instead of onInitActivation). List both triggers to handle setup and teardown in one reaction:

triggers: ["onActivation", "onEndActivation"],
activationCode: async function (triggerType, triggerData, reactorToken, item, activationName, api) {
    if (triggerType === "onEndActivation") {
        await teardownEffect(reactorToken, item, api);
        return;
    }
    await setupEffect(reactorToken, item, api);
    await api.setItemAsActivated(item, reactorToken, "Protocol", "Collapse the Defense Net");
}

Other triggers on the same item can gate themselves by checking getActivatedItems:

evaluate: function (triggerType, triggerData, reactorToken, item, activationName, api) {
    if (!api.getActivatedItems(reactorToken)?.some(i => i.id === item.id))
        return false;
    ...
}

Forced teardown from a separate trigger (e.g. the wearer gets stunned and the field collapses) calls endItemActivation directly, or calls the teardown helper and then endItemActivation to clean up the end-action row.

Defense Net in startups/itemActivations.js is the reference implementation (onActivation for setup + onEndActivation teardown, onStatusApplied with getActivatedItems to force-collapse when stunned/jammed, onHeatGain / onTechMiss reactions gated by getActivatedItems).



13. Registration Paths

There are four ways to register an activation. They all end up in the same dispatcher.

A. Activation Manager UI

Module Settings > Activation Manager. Create item or general activations through forms. Stored in customReactions / generalReactions world settings. Best for one-offs and homebrew.

B. Module-time registration

Hooks.on("lancer-automations.ready", (api) => {
    api.registerDefaultItemReactions({
        "lid_of_the_item": {
            category: "Homebrew",
            itemType: "npc_feature",
            reactions: [{
                triggers: ["onActivation"],
                onlyOnSourceMatch: true,
                triggerSelf: true,
                triggerOther: false,
                autoActivate: true,
                activationType: "code",
                activationMode: "instead",
                activationCode: async function (triggerType, triggerData, reactorToken, item, activationName, api) {
                }
            }]
        }
    });

    api.registerDefaultGeneralReactions({
        "My General Reaction": { reactions: [] }
    });
});

External registrations are merged with last-write-wins semantics: re-registering the same key replaces the previous entry.

C. Built-in defaults

The same two functions are used by startups/itemActivations.js for the bundled NPC/feature automations, with the same result as B.

D. Startup scripts

Module Settings > Activation Manager > Startup tab. Arbitrary code blocks run once on ready. Useful for registering helper functions on the API (api.registerUserHelper("myHelper", fn)) so your activation code can call them by name.

Override order

For a given key (LID or general name), the resolution order is roughly: user UI definitions override external registrations override built-in defaults. Re-registrations within the same path replace.



14. Caches and Invalidation

If a reaction you just changed or registered doesn't take effect, clear the caches:

Hooks.callAll("lancer-automations.clearCaches");

That hook is the only thing that invalidates them. Nothing about an item or an actor changing does. Three caches listen to it:

  • Flat general reaction list. Built once, used for every general-reaction sweep.

  • Per-trigger non-action list. The general reactions that listen to one trigger, keyed by trigger type.

  • Per-trigger scene list. The same, for reactions with sceneReactor on.

The UI fires the hook on every save, and so does registerDefaultItemReactions / registerDefaultGeneralReactions. You only need to call it by hand when you have edited a registry from the console.

There is no per-actor item cache: getReactionItems rebuilds an actor's list from actor.items on every trigger. If a reaction stopped firing after an item change, the cache is not why.

Separate from all three, the compiled function cache holds the functions built from UI code strings, keyed by code, argument list and source name. The hook does not clear it. The UI's own save path clears it and then fires the hook.