Foundry VTT Version Compatibility (V12/V13/V15+)
Use compatibility wrappers to avoid deprecation warnings when APIs move from globals to namespaces across Foundry versions.
When to Use This Skill
Invoke this skill when:
✅ Use Compat Wrappers For:
- Importing Foundry classes - ActorSheet, ItemSheet, TextEditor
- Registering sheets - Actor sheets, item sheets, document sheets
- Loading templates - Handlebars template loading
- Enriching HTML - TextEditor.enrichHTML for journal content
- Generating IDs - randomID() for unique identifiers
- Any Foundry API - That has moved or will move to namespaces
❌ Don't Use Compat Wrappers For:
- Stable globals -
game,ui,CONFIG,Hooks - Project-specific code - Your own classes and functions
- One-off migrations - If only targeting a single Foundry version
- Styling/layout - CSS and templates (not API drift)
The Problem: API Migration Across Versions
What Changed Across Foundry Versions
Foundry V11/V12 (Legacy):
// APIs available as globals
ActorSheet
ItemSheet
TextEditor.enrichHTML()
loadTemplates()
renderTemplate()
randomID()
Actors.registerSheet()
Foundry V13+ (Namespaced):
// APIs moved to namespaces
foundry.appv1.sheets.ActorSheet
foundry.appv1.sheets.ItemSheet
foundry.applications.ux.TextEditor.implementation.enrichHTML()
foundry.applications.handlebars.loadTemplates()
foundry.applications.handlebars.renderTemplate()
foundry.utils.randomID()
foundry.applications.api.DocumentSheetConfig.registerSheet()
Foundry V15+ (Legacy Removed):
Globals will be REMOVED entirely
↓
Direct global access will break
↓
Must use namespaced APIs only
Why This Breaks Code
// ❌ This worked in V11/V12, will BREAK in V15+
import { ActorSheet } from "somewhere"; // No longer a global!
class MySheet extends ActorSheet {
// ...
}
// ❌ This throws deprecation warnings in V13
TextEditor.enrichHTML(content, options);
// ❌ This will stop working in V15
Actors.registerSheet("my-module", MySheet, { makeDefault: true });
Solution: Compatibility Wrappers
Three Core Patterns
- Try modern first, fallback to legacy - Nullish coalescing (
??) - Cache resolved classes - Avoid repeated lookups
- Throw clear errors - Don't silently fail if neither exists
Step-by-Step Implementation
Step 1: Create Compatibility Module
File: scripts/compat.js
/**
* Compatibility helpers for Foundry V12/V13/V15+
* Prefer modern namespaced APIs, fallback to legacy globals
*/
/**
* Get ActorSheet class (modern or legacy)
* @returns {class} ActorSheet constructor
*/
export function getActorSheetClass() {
// Try V13+ namespace first
const modern = foundry?.appv1?.sheets?.ActorSheet;
if (modern) return modern;
// Fallback to V11/V12 global
if (typeof ActorSheet !== 'undefined') return ActorSheet;
throw new Error("Unable to resolve ActorSheet class");
}
/**
* Get ItemSheet class (modern or legacy)
* @returns {class} ItemSheet constructor
*/
export function getItemSheetClass() {
return foundry?.appv1?.sheets?.ItemSheet ?? ItemSheet;
}
/**
* Enrich HTML content (journal entries, descriptions)
* @param {string} content - Raw HTML/markdown content
* @param {object} options - Enrichment options
* @returns {Promise<string>} Enriched HTML
*/
export function enrichHTML(content, options = {}) {
// Try V13+ namespace
const textEditor = foundry?.applications?.ux?.TextEditor?.implementation;
if (textEditor?.enrichHTML) {
return textEditor.enrichHTML(content, options);
}
// Fallback to V11/V12 global
if (typeof TextEditor !== 'undefined' && TextEditor.enrichHTML) {
return TextEditor.enrichHTML(content, options);
}
throw new Error("Unable to resolve TextEditor.enrichHTML");
}
/**
* Generate random ID
* @returns {string} Random ID
*/
export function generateRandomId() {
const randomIdFn = foundry?.utils?.randomID ?? randomID;
if (!randomIdFn) {
throw new Error("Unable to resolve randomID generator");
}
return randomIdFn();
}
Pattern:
// Template for adding new compat functions
export function getAPIClass() {
// 1. Try modern namespace
const modern = foundry?.path?.to?.API;
if (modern) return modern;
// 2. Fallback to legacy global
if (typeof LegacyGlobal !== 'undefined') return LegacyGlobal;
// 3. Throw clear error
throw new Error("Unable to resolve API");
}
Step 2: Sheet Registration Compatibility
File: scripts/compat-helpers.js
import { getActorSheetClass, getItemSheetClass } from "./compat.js";
// Cache DocumentSheetConfig to avoid repeated lookups
let cachedSheetConfig;
/**
* Get DocumentSheetConfig (modern V13+) or null
* @returns {object|null}
*/
function getSheetConfig() {
if (cachedSheetConfig) return cachedSheetConfig;
// Try multiple V13+ namespace locations
const apiConfig =
foundry?.applications?.apps?.DocumentSheetConfig ??
foundry?.applications?.config?.DocumentSheetConfig ??
foundry?.applications?.api?.DocumentSheetConfig;
cachedSheetConfig = apiConfig ?? null;
return cachedSheetConfig;
}
/**
* Get legacy Actors collection (V11/V12)
* @returns {object}
*/
function getActorsCollectionLegacy() {
return foundry?.documents?.collections?.Actors ?? Actors;
}
/**
* Get legacy Items collection (V11/V12)
* @returns {object}
*/
function getItemsCollectionLegacy() {
return foundry?.documents?.collections?.Items ?? Items;
}
/**
* Register actor sheet (compatible across versions)
* @param {string} namespace - Module ID
* @param {class} sheetClass - Sheet constructor
* @param {object} options - Registration options
*/
export function registerActorSheet(namespace, sheetClass, options = {}) {
const sheetConfig = getSheetConfig();
// Try V13+ API
if (sheetConfig?.registerSheet) {
return sheetConfig.registerSheet(
CONFIG.Actor.documentClass,
namespace,
sheetClass,
options
);
}
// Fallback to V11/V12 API
return getActorsCollectionLegacy()?.registerSheet?.(
namespace,
sheetClass,
options
);
}
/**
* Unregister actor sheet (compatible across versions)
*/
export function unregisterActorSheet(namespace, sheetClass) {
const sheetConfig = getSheetConfig();
if (sheetConfig?.unregisterSheet) {
return sheetConfig.unregisterSheet(
CONFIG.Actor.documentClass,
namespace,
sheetClass
);
}
return getActorsCollectionLegacy()?.unregisterSheet?.(namespace, sheetClass);
}
/**
* Register item sheet (compatible across versions)
*/
export function registerItemSheet(namespace, sheetClass, options = {}) {
const sheetConfig = getSheetConfig();
if (sheetConfig?.registerSheet) {
return sheetConfig.registerSheet(
CONFIG.Item.documentClass,
namespace,
sheetClass,
options
);
}
return getItemsCollectionLegacy()?.registerSheet?.(
namespace,
sheetClass,
options
);
}
/**
* Unregister item sheet (compatible across versions)
*/
export function unregisterItemSheet(namespace, sheetClass) {
const sheetConfig = getSheetConfig();
if (sheetConfig?.unregisterSheet) {
return sheetConfig.unregisterSheet(
CONFIG.Item.documentClass,
namespace,
sheetClass
);
}
return getItemsCollectionLegacy()?.unregisterSheet?.(namespace, sheetClass);
}
Step 3: Template Loading Compatibility
/**
* Load Handlebars templates (compatible across versions)
* @param {Array<string>} paths - Template paths
* @returns {Promise}
*/
export function loadHandlebarsTemplates(paths) {
// Try V13+ namespace
const loader = foundry?.applications?.handlebars?.loadTemplates;
if (loader) {
return loader(paths);
}
// Fallback to V11/V12 global
if (typeof loadTemplates !== 'undefined') {
return loadTemplates(paths);
}
throw new Error("Unable to resolve Handlebars template loader");
}
/**
* Render Handlebars template (compatible across versions)
* @param {string} path - Template path
* @param {object} data - Template data
* @returns {Promise<string>}
*/
export function renderHandlebarsTemplate(path, data) {
// Try V13+ namespace
const renderer = foundry?.applications?.handlebars?.renderTemplate;
if (renderer) {
return renderer(path, data);
}
// Fallback to V11/V12 global
if (typeof renderTemplate !== 'undefined') {
return renderTemplate(path, data);
}
throw new Error("Unable to resolve Handlebars template renderer");
}
Step 4: Use Compat Wrappers in Your Code
File: scripts/module.js (Entry point)
import { registerActorSheet, registerItemSheet } from "./compat-helpers.js";
import { loadHandlebarsTemplates } from "./compat-helpers.js";
import { BladesAlternateActorSheet } from "./blades-alternate-actor-sheet.js";
import { BladesAlternateItemSheet } from "./blades-alternate-item-sheet.js";
const MODULE_ID = "my-module";
Hooks.once("init", async function() {
console.log("My Module | Initializing");
// Load templates (compat)
await loadHandlebarsTemplates([
"modules/my-module/templates/actor-sheet.html",
"modules/my-module/templates/item-sheet.html",
]);
});
Hooks.once("ready", async function() {
// Register sheets (compat)
// Why ready hook? V13+ requires DocumentSheetConfig to be available
// which isn't ready during init
registerActorSheet(
MODULE_ID,
BladesAlternateActorSheet,
{
types: ["character"],
makeDefault: true,
label: "Alternate Character Sheet"
}
);
registerItemSheet(
MODULE_ID,
BladesAlternateItemSheet,
{
types: ["item"],
makeDefault: false,
label: "Alternate Item Sheet"
}
);
});
File: scripts/blades-alternate-actor-sheet.js
import { getActorSheetClass } from "./compat.js";
import { enrichHTML } from "./compat.js";
// Get base class via compat wrapper
const ActorSheet = getActorSheetClass();
export class BladesAlternateActorSheet extends ActorSheet {
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["bitd-alt", "sheet", "actor"],
template: "modules/my-module/templates/actor-sheet.html",
width: 900,
height: 800,
});
}
async getData() {
const data = await super.getData();
// Enrich description (compat)
if (data.actor.system.description) {
data.enrichedDescription = await enrichHTML(
data.actor.system.description,
{ secrets: data.editable }
);
}
return data;
}
}
Compatibility Patterns
Pattern 1: Simple Class Resolution
// Get class, throw if not found
export function getAPIClass() {
const api = foundry?.new?.path?.API ?? LegacyGlobalAPI;
if (!api) {
throw new Error("Unable to resolve API");
}
return api;
}
Pattern 2: Cached Resolution
// Cache expensive lookups
let cachedConfig;
function getConfig() {
if (cachedConfig) return cachedConfig;
const config =
foundry?.new?.path?.Config ??
foundry?.another?.path?.Config ??
LegacyConfig;
cachedConfig = config ?? null;
return cachedConfig;
}
Pattern 3: Method Delegation
// Wrap methods that moved
export function someMethod(...args) {
const api = foundry?.new?.path?.API?.implementation;
if (api?.someMethod) {
return api.someMethod(...args);
}
if (typeof LegacyAPI !== 'undefined' && LegacyAPI.someMethod) {
return LegacyAPI.someMethod(...args);
}
throw new Error("Unable to resolve someMethod");
}
Pattern 4: Optional Chaining for Safety
// Use ?. to safely traverse nested paths
export function getDeepAPI() {
return (
foundry?.level1?.level2?.level3?.API ??
OldGlobal?.level1?.API ??
LegacyAPI
);
}
Why Defer Sheet Registration to ready Hook?
The Problem
// ❌ BAD: Registering in init hook
Hooks.once("init", function() {
registerActorSheet(MODULE_ID, MySheet, { ... });
});
// Result in V13+:
// Error: DocumentSheetConfig is not available yet!
The Solution
// ✅ GOOD: Register in ready hook
Hooks.once("ready", function() {
registerActorSheet(MODULE_ID, MySheet, { ... });
});
Why?
- V11/V12: Sheet registration works in
inithook (globals available) - V13+:
DocumentSheetConfigisn't initialized until afterinit - V15+: Legacy globals won't exist, must use
DocumentSheetConfig
Solution: Always register sheets in ready hook for V13+ compatibility
Common Use Cases
Use Case 1: Extend Base Sheet Class
import { getActorSheetClass } from "./compat.js";
const ActorSheet = getActorSheetClass();
export class MyActorSheet extends ActorSheet {
// No deprecation warnings!
}
Use Case 2: Enrich Journal Content
import { enrichHTML } from "./compat.js";
async function displayJournalEntry(content) {
const enriched = await enrichHTML(content, {
secrets: game.user.isGM,
documents: true,
links: true,
});
return enriched;
}
Use Case 3: Load Templates on Init
import { loadHandlebarsTemplates } from "./compat-helpers.js";
Hooks.once("init", async function() {
await loadHandlebarsTemplates([
"modules/my-module/templates/actor-sheet.html",
"modules/my-module/templates/parts/abilities.html",
"modules/my-module/templates/parts/items.html",
]);
});
Use Case 4: Generate Unique IDs
import { generateRandomId } from "./compat.js";
function createNewItem() {
const item = {
_id: generateRandomId(),
name: "New Item",
type: "item",
};
return item;
}
Adding New Compat Functions
Recipe for New API
-
Identify the move
- V11/V12 global:
OldAPI - V13+ namespace:
foundry.new.path.NewAPI
- V11/V12 global:
-
Add compat function
export function getNewAPI() { return foundry?.new?.path?.NewAPI ?? OldAPI; } -
Update imports
// Before import { OldAPI } from "somewhere"; // After import { getNewAPI } from "./compat.js"; const OldAPI = getNewAPI(); -
Test across versions
- V11: Should use legacy global
- V13: Should use modern namespace
- No deprecation warnings in either
API Migration Checklist
Common APIs that have moved or will move:
-
ActorSheet→foundry.appv1.sheets.ActorSheet -
ItemSheet→foundry.appv1.sheets.ItemSheet -
TextEditor.enrichHTML→foundry.applications.ux.TextEditor.implementation.enrichHTML -
loadTemplates→foundry.applications.handlebars.loadTemplates -
renderTemplate→foundry.applications.handlebars.renderTemplate -
randomID→foundry.utils.randomID -
Actors.registerSheet→DocumentSheetConfig.registerSheet -
Items.registerSheet→DocumentSheetConfig.registerSheet -
Dialog→DialogV2(see dialog-compat skill)
Testing Across Versions
Manual Testing
1. Test in Foundry V11 (if supporting)
- Check console for errors
- Verify sheets register correctly
- Confirm templates load
2. Test in Foundry V12
- Same checks as V11
- Look for deprecation warnings
3. Test in Foundry V13+
- No deprecation warnings
- All features work
- Modern APIs used (check network/console)
Console Verification
// In browser console, verify which API is being used
// V11/V12 - Should use globals
console.log(typeof ActorSheet !== 'undefined'); // true
// V13+ - Should use namespaces
console.log(foundry?.appv1?.sheets?.ActorSheet); // class ActorSheet
Common Pitfalls
❌ Pitfall 1: Using Globals Directly
// BAD: Will throw deprecation warnings in V13, break in V15
class MySheet extends ActorSheet {
// ...
}
Fix: Use compat wrapper
// GOOD
import { getActorSheetClass } from "./compat.js";
const ActorSheet = getActorSheetClass();
class MySheet extends ActorSheet {
// ...
}
❌ Pitfall 2: Registering Sheets in Init Hook
// BAD: Breaks in V13+
Hooks.once("init", function() {
Actors.registerSheet(MODULE_ID, MySheet, { ... });
});
Fix: Use ready hook + compat wrapper
// GOOD
import { registerActorSheet } from "./compat-helpers.js";
Hooks.once("ready", function() {
registerActorSheet(MODULE_ID, MySheet, { ... });
});
❌ Pitfall 3: Not Handling Multiple Namespace Locations
// BAD: Assumes single namespace location
export function getConfig() {
return foundry?.applications?.api?.Config ?? LegacyConfig;
}
// Problem: Config might be in different namespace!
Fix: Check multiple locations
// GOOD
export function getConfig() {
return (
foundry?.applications?.api?.Config ??
foundry?.applications?.apps?.Config ??
foundry?.applications?.config?.Config ??
LegacyConfig
);
}
❌ Pitfall 4: Silent Fallback Failures
// BAD: Returns undefined if both fail
export function getAPI() {
return foundry?.new?.API ?? OldAPI;
}
// Calling code will crash with cryptic error later
Fix: Throw clear error
// GOOD
export function getAPI() {
const api = foundry?.new?.API ?? OldAPI;
if (!api) {
throw new Error("Unable to resolve API - unsupported Foundry version?");
}
return api;
}
Quick Checklist
Before using Foundry APIs:
- Identified if API has moved to namespace
- Created compat wrapper function
- Used compat wrapper instead of direct global
- Moved sheet registration to
readyhook - Cached expensive lookups (configs, classes)
- Threw clear errors if API not found
- Tested in V12 and V13+ (if targeting both)
- No deprecation warnings in console
References
- Implementation:
scripts/compat.js- Core compatibility wrappers - Helpers:
scripts/compat-helpers.js- Sheet registration, template loading - Guide:
docs/compat-helpers-guide.md- Usage examples - Foundry API Docs: Application V1 Migration
For BitD Alternate Sheets:
- All sheet classes extend via
getActorSheetClass()/getItemSheetClass() - Sheet registration happens in
readyhook viaregisterActorSheet() - Template loading uses
loadHandlebarsTemplates()ininithook - HTML enrichment uses
enrichHTML()wrapper - Supports Foundry V12 minimum, tested through V13
- Prepared for V15+ when legacy globals are removed
