Skip to content
fvtt-hooks logo

Foundry VTT Hooks

fvtt-hooks

This skill should be used when registering hooks, creating custom hooks for module APIs, debugging hook execution, handling async in hooks, or preventing memory leaks from unclean hook removal. Covers Hooks.on/once/call/callAll, lifecycle order, and common pitfalls.

SKILL.md

Full skill instructions

Foundry VTT Hooks

Domain: Foundry VTT Module/​System Development Status: Production-Ready Last Updated: 2026-01-04

Overview

Foundry VTT uses an event-driven architecture where hooks allow modules to intercept and respond to VTT workflows. Understanding hooks is fundamental to all Foundry development.

When to Use This Skill

  • Registering event handlers for document changes
  • Creating module APIs that other packages can consume
  • Debugging hook execution order
  • Preventing memory leaks from orphaned hooks
  • Understanding async limitations in hooks

Core Concepts

MethodPurposeCancellablePersists
Hooks.on(name, fn)Register persistent listenerN/​AYes
Hooks.once(name, fn)Register one-time listenerN/​ANo
Hooks.call(name, ...args)Trigger hook (stoppable)YesN/​A
Hooks.callAll(name, ...args)Trigger hook (all run)NoN/​A
Hooks.off(name, id)Unregister by IDN/​AN/​A

Lifecycle Hooks (Execution Order)

These fire once per client connection, in this order:

init → i18nInit → setup → ready
HookWhenUse For
initFoundry initializingRegister sheets, settings, CONFIG
i18nInitTranslations loadedLocalization-dependent setup
setupBefore Documents/​UI/​CanvasPre-game state setup
readyGame fully readyWorld initialization, game data access
// Correct registration order
Hooks.once("init", () => {
  // Register settings
  game.settings.register("my-module", "mySetting", { /​* ... */ });

  // Register document sheets
  DocumentSheetConfig.registerSheet(Actor, "my-module", MyActorSheet, {
    makeDefault: true
  });
});

Hooks.once("ready", () => {
  // Safe to access game.actors, game.users, etc.
  console.log(`${game.actors.size} actors in world`);
});

Document Hooks

All document types follow this pattern:

HookTrigger MethodCan Cancel
preCreate{Doc}Hooks.callYes
create{Doc}Hooks.callAllNo
preUpdate{Doc}Hooks.callYes
update{Doc}Hooks.callAllNo
preDelete{Doc}Hooks.callYes
delete{Doc}Hooks.callAllNo
// Prevent update if condition met
Hooks.on("preUpdateActor", (actor, change, options, userId) => {
  if (change.system?.hp?.value < 0) {
    ui.notifications.warn("HP cannot be negative");
    return false; // Cancels the update
  }
});

// React after update
Hooks.on("updateActor", (actor, change, options, userId) => {
  if (change.system?.hp?.value === 0) {
    ChatMessage.create({ content: `${actor.name} has fallen!` });
  }
});

Render Hooks

Render hooks fire for the entire inheritance chain:

// renderActorSheet → renderDocumentSheet → renderFormApplication → renderApplication

// ApplicationV1 signature
Hooks.on("renderActorSheet", (app, html, data) => {
  html.find(".header").append("<button>Custom</​button>");
});

// ApplicationV2 signature (V12+)
Hooks.on("renderActorSheetV2", (app, html, context, options) => {
  // html is the rendered element
});

Common Pitfalls

1. Async Functions Cannot Cancel Hooks

CRITICAL: Hooks never await callbacks. Async functions return a Promise, not false.

// WRONG - Won't prevent the update!
Hooks.on("preUpdateActor", async (actor, change, options, userId) => {
  const allowed = await someAsyncCheck();
  if (!allowed) return false; // Returns Promise, not false!
});

// CORRECT - Synchronous check for cancellation
Hooks.on("preUpdateActor", (actor, change, options, userId) => {
  if (!someCondition) return false; // Actually prevents update
});

2. Memory Leaks from Orphaned Hooks

Hooks keep references to callbacks, preventing garbage collection:

// BAD - Hook persists after app closes
class MyApp extends Application {
  constructor() {
    super();
    Hooks.on("updateActor", this._onUpdate.bind(this));
  }
}

// GOOD - Store ID and clean up
class MyApp extends Application {
  constructor() {
    super();
    this._hookId = Hooks.on("updateActor", this._onUpdate.bind(this));
  }

  close(options) {
    Hooks.off("updateActor", this._hookId);
    return super.close(options);
  }
}

3. Object Mutation vs Re-assignment

Objects are passed by reference - mutation works, re-assignment doesn't:

// WORKS - Mutation affects original
Hooks.on("preUpdateActor", (actor, change, options, userId) => {
  change.name = "Modified"; // Changes the actual update
});

// DOESN'T WORK - Re-assignment breaks reference
Hooks.on("preUpdateActor", (actor, change, options, userId) => {
  change = { name: "New" }; // Local variable only!
});

4. Hooks Fire Per-Client

Each client runs hooks independently. Check userId to avoid duplicate actions:

// BAD - All clients try to create the item
Hooks.on("createActor", (actor, options, userId) => {
  actor.createEmbeddedDocuments("Item", [itemData]);
});

// GOOD - Only triggering client acts
Hooks.on("createActor", (actor, options, userId) => {
  if (userId !== game.user.id) return;
  actor.createEmbeddedDocuments("Item", [itemData]);
});

5. Hook Context (this) Issues

// WRONG - Can't unregister by function reference
Hooks.on("updateActor", this.onUpdate);
Hooks.off("updateActor", this.onUpdate); // Fails!

// WRONG - bind() creates new function each time
Hooks.on("updateActor", this.onUpdate.bind(this));
Hooks.off("updateActor", this.onUpdate.bind(this)); // Different function!

// CORRECT - Use hook ID
this._hookId = Hooks.on("updateActor", this.onUpdate.bind(this));
Hooks.off("updateActor", this._hookId); // Works

Creating Custom Hooks for Module APIs

Basic Pattern

// my-module.js
Hooks.once("init", () => {
  const api = {
    registerExtension: (config) => { /​* ... */ },
    doAction: (data) => { /​* ... */ }
  };

  // Expose API
  game.modules.get("my-module").api = api;

  // Announce readiness
  Hooks.callAll("myModuleReady", api);
});

// Other modules consume it
Hooks.once("myModuleReady", (api) => {
  api.registerExtension({ name: "My Extension" });
});

Allowing Cancellation

class MySystem {
  static performAction(data) {
    // Allow other modules to prevent action
    const allowed = Hooks.call("myModule.beforeAction", data);
    if (allowed === false) return null;

    const result = this._doWork(data);

    // Notify completion (cannot be cancelled)
    Hooks.callAll("myModule.afterAction", result);

    return result;
  }
}

Naming Convention

Use namespaced names: moduleName.eventName

  • combatTracker.turnChanged
  • tokenMagic.effectApplied
  • myModule.ready

Debugging Hooks

Enable Debug Mode

// In browser console
CONFIG.debug.hooks = true;

// Toggle macro
CONFIG.debug.hooks = !CONFIG.debug.hooks;
console.warn("Hook debugging:", CONFIG.debug.hooks);

Debug Specific Hook

// Log arguments for a specific hook once
Hooks.once("updateActor", (...args) => console.log("updateActor args:", args));

Developer Mode Module

Use the Developer Mode module for persistent debug flag management without shipping debug code.

Implementation Checklist

  • Use Hooks.once for init/​setup/​ready (not Hooks.on)
  • Store hook IDs for any persistent hooks
  • Clean up hooks in close() or destruction methods
  • Check userId === game.user.id before document operations
  • Never use async for pre* hooks that need cancellation
  • Mutate objects, don't re-assign them
  • Use namespaced names for custom hooks
  • Use Hooks.call for cancellable events, Hooks.callAll for notifications

Quick Reference: Common Hook Signatures

// Document hooks
Hooks.on("preCreateActor", (document, data, options, userId) => {});
Hooks.on("createActor", (document, options, userId) => {});
Hooks.on("preUpdateActor", (document, change, options, userId) => {});
Hooks.on("updateActor", (document, change, options, userId) => {});
Hooks.on("preDeleteActor", (document, options, userId) => {});
Hooks.on("deleteActor", (document, options, userId) => {});

// Render hooks (V1)
Hooks.on("renderActorSheet", (app, html, data) => {});

// Render hooks (V2)
Hooks.on("renderActorSheetV2", (app, html, context, options) => {});

// Canvas hooks
Hooks.on("canvasReady", (canvas) => {});
Hooks.on("canvasPan", (canvas, position) => {});

References


Last Updated: 2026-01-04 Status: Production-Ready Maintainer: ImproperSubset

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View