xiaohongshu
xiaohongshu
fvtt-data-migrations
This skill should be used when moving data between storage locations, changing data structures, renaming fields, or removing deprecated data. Covers schema versioning, safe migration methods, the Foundry unset operator, and idempotent migrations.
Full skill instructions
Implement safe, version-controlled data migrations for Foundry VTT modules when changing data structures or storage locations.
Invoke this skill when implementing changes that require migrating existing user data:
Moving data between storage locations:
actor.system.field → actor.flags.myModule.fieldactor.system.field → embedded itemChanging data structures:
Changing data types:
Removing deprecated data:
Foundry version compatibility:
Additive changes only:
Non-breaking changes:
Foundry modules track data structure versions using a schemaVersion setting:
// In settings.js - register the version tracker
game.settings.register("my-module", "schemaVersion", {
name: "Schema Version",
scope: "world", // World-level (not per-client)
config: false, // Hidden from UI
type: Number,
default: 0, // 0 = never migrated
});
How it works:
ready hookcurrentVersion >= targetVersion, migration is skippedWorld Created (v0)
↓
First Load → ready hook
↓
Migration.migrate() called
↓
Check: currentVersion (0) < targetVersion (1)?
↓ YES
Run migration steps
↓
Set schemaVersion = 1
↓
Show completion notification
↓
Future loads: currentVersion (1) >= targetVersion (1) → SKIP
Ask yourself:
Example scenarios:
// Scenario A: Moving system field to flag
OLD: actor.system["background-details"]
NEW: actor.flags["bitd-alternate-sheets"].background_details
// Scenario B: Changing structure
OLD: actor.flags.myModule["equipped-items"] = [{ id: "abc" }, { id: "def" }]
NEW: actor.flags.myModule["equipped-items"] = { "abc": {...}, "def": {...} }
// Scenario C: Cleaning up orphaned data
OLD: actor.flags.myModule.abilityProgress = { "Reflexes": 2, "abc123": 1 }
NEW: actor.flags.myModule.abilityProgress = { "abc123": 1 } // Remove name-based keys
In scripts/migration.js:
export class Migration {
static async migrate() {
const currentVersion = game.settings.get(MODULE_ID, "schemaVersion") || 0;
const targetVersion = 2; // ← INCREMENT THIS (was 1, now 2)
if (currentVersion < targetVersion) {
// ... migration logic
}
}
}
Version numbering:
targetVersion = 1 (first migration)Add a new static method to Migration class:
static async migrateFieldName(actor) {
const oldValue = foundry.utils.getProperty(actor, "system.old-field");
const newValue = actor.getFlag(MODULE_ID, "new_field");
// Check if migration needed
if (!oldValue) return; // No old data to migrate
if (newValue) {
// New data already exists - just clean up old
await actor.update({ "system.-=old-field": null });
return;
}
// Perform migration
const updates = {};
updates[`flags.${MODULE_ID}.new_field`] = oldValue;
updates["system.-=old-field"] = null;
await actor.update(updates);
console.log(`[MyModule] Migrated old-field for ${actor.name}`);
}
Migration method naming:
migrate{FeatureName} - Descriptive of what's being migratedmigrateEquippedItems, migrateAbilityProgress, migrateLegacyFieldsAdd to the migration sequence:
static async migrate() {
const currentVersion = game.settings.get(MODULE_ID, "schemaVersion") || 0;
const targetVersion = 2;
if (currentVersion < targetVersion) {
ui.notifications.info(
"My Module: Migrating data, please wait..."
);
for (const actor of game.actors) {
if (actor.type !== "character") continue;
// Step 1: Existing migration
await this.migrateEquippedItems(actor);
// Step 2: NEW migration (add here)
await this.migrateFieldName(actor);
}
await game.settings.set(MODULE_ID, "schemaVersion", targetVersion);
ui.notifications.info("My Module: Migration complete.");
}
}
Order considerations:
Testing checklist:
Create test world with OLD data structure:
game.actors.getName("Test").update({ "system.old-field": "test" })Trigger migration:
ready hook)Verify migration results:
game.actors.getName("Test").getFlag("my-module", "new_field")game.actors.getName("Test").system["old-field"] (should be undefined)Verify migration runs ONCE:
Test with multiple actors:
static async migrateLegacyFields(actor) {
const updates = {};
let changed = false;
// Get old and new locations
const oldValue = foundry.utils.getProperty(actor, "system.old-field");
const newValue = actor.getFlag(MODULE_ID, "new_field");
if (oldValue && !newValue) {
// Old exists, new doesn't - migrate
updates[`flags.${MODULE_ID}.new_field`] = oldValue;
updates["system.-=old-field"] = null;
changed = true;
} else if (oldValue && newValue) {
// Both exist - favor new, clean up old
updates["system.-=old-field"] = null;
changed = true;
}
if (changed) {
await actor.update(updates);
console.log(`[MyModule] Migrated old-field for ${actor.name}`);
}
}
Key points:
actor.update() callstatic async migrateEquippedItems(actor) {
const equipped = actor.getFlag(MODULE_ID, "equipped-items");
// Check if still in old format (array)
if (Array.isArray(equipped)) {
const newMap = {};
for (const item of equipped) {
if (item && item.id) {
newMap[item.id] = item;
}
}
await actor.setFlag(MODULE_ID, "equipped-items", newMap);
console.log(`[MyModule] Migrated equipped items for ${actor.name}`);
}
}
Key points:
Array.isArray() to detect old formatsetFlag() to replace entire structurestatic async migrateAbilityProgress(actor) {
const progressMap = actor.getFlag(MODULE_ID, "abilityProgress") || {};
if (foundry.utils.isEmpty(progressMap)) return;
let changed = false;
const updates = {};
for (const [key, value] of Object.entries(progressMap)) {
// Foundry IDs are always 16 characters
if (key.length !== 16) {
// This is likely a name-based key (orphaned) - remove it
updates[`flags.${MODULE_ID}.abilityProgress.-=${key}`] = null;
changed = true;
}
}
if (changed) {
await actor.update(updates);
console.log(`[MyModule] Cleaned orphaned flags for ${actor.name}`);
}
}
Key points:
-= syntax to remove specific keys: "flags.module.field.-=keyToRemove"static async migrateLegacyFields(actor) {
const updates = {};
let changed = false;
// Migration 1: background-details
const oldDetails = foundry.utils.getProperty(actor, "system.background-details");
const newDetails = actor.getFlag(MODULE_ID, "background_details");
if (oldDetails && !newDetails) {
updates[`flags.${MODULE_ID}.background_details`] = oldDetails;
updates["system.-=background-details"] = null;
changed = true;
}
// Migration 2: vice-purveyor
const oldPurveyor = foundry.utils.getProperty(actor, "system.vice-purveyor");
const newPurveyor = actor.getFlag(MODULE_ID, "vice_purveyor");
if (oldPurveyor && !newPurveyor) {
updates[`flags.${MODULE_ID}.vice_purveyor`] = oldPurveyor;
updates["system.-=vice-purveyor"] = null;
changed = true;
}
// Single batched update for all changes
if (changed) {
await actor.update(updates);
console.log(`[MyModule] Migrated legacy fields for ${actor.name}`);
}
}
Key points:
updates objectactor.update() call at the endupdates["system.field"] = newValue; // Set system field
updates[`flags.${MODULE_ID}.field`] = newValue; // Set flag
updates["system.nested.deep.field"] = newValue; // Set nested field
updates["system.-=oldField"] = null; // Remove system field
updates[`flags.${MODULE_ID}.-=oldFlag`] = null; // Remove flag
updates["flags.module.map.-=keyName"] = null; // Remove map key
The -= syntax:
null or undefined (which leaves the key)// Safe nested property access (returns undefined if any level missing)
const value = foundry.utils.getProperty(actor, "system.deep.nested.field");
// vs direct access (throws if intermediate property missing)
const value = actor.system.deep.nested.field; // Error if 'deep' undefined!
for (const actor of game.actors) {
if (actor.type !== "character") continue; // Only migrate characters
await this.migrateCharacterFields(actor);
}
for (const actor of game.actors) {
// Migrate all actors regardless of type
await this.migrateCommonFields(actor);
}
for (const actor of game.actors) {
if (actor.type === "character") {
await this.migrateCharacterFields(actor);
} else if (actor.type === "crew") {
await this.migrateCrewFields(actor);
}
}
// Start notification
ui.notifications.info("My Module: Migrating data, please wait...");
// After migration completes
ui.notifications.info("My Module: Migration complete.");
// Error notification (if migration fails)
ui.notifications.error("My Module: Migration failed. See console for details.");
When to notify:
// Per-actor migration success
console.log(`[MyModule] Migrated equipped items for ${actor.name}`);
// Overall migration start
console.log("[MyModule] Starting migration to schema v2");
// Debug information
console.log(`[MyModule] Found ${Object.keys(progressMap).length} progress entries`);
Logging best practices:
[MyModule]console.log for success (not console.error)static async renameFlag(actor) {
const oldValue = actor.getFlag(MODULE_ID, "oldName");
const newValue = actor.getFlag(MODULE_ID, "newName");
if (oldValue && !newValue) {
await actor.setFlag(MODULE_ID, "newName", oldValue);
await actor.unsetFlag(MODULE_ID, "oldName");
console.log(`[MyModule] Renamed flag for ${actor.name}`);
}
}
static async combineFlags(actor) {
const flag1 = actor.getFlag(MODULE_ID, "flag1");
const flag2 = actor.getFlag(MODULE_ID, "flag2");
const combined = actor.getFlag(MODULE_ID, "combined");
if ((flag1 || flag2) && !combined) {
const newValue = {
part1: flag1 || {},
part2: flag2 || {}
};
const updates = {};
updates[`flags.${MODULE_ID}.combined`] = newValue;
updates[`flags.${MODULE_ID}.-=flag1`] = null;
updates[`flags.${MODULE_ID}.-=flag2`] = null;
await actor.update(updates);
console.log(`[MyModule] Combined flags for ${actor.name}`);
}
}
static async migrateItemData(actor) {
const updates = [];
for (const item of actor.items) {
if (item.type === "ability") {
const oldField = item.system.oldField;
if (oldField) {
updates.push({
_id: item.id,
"system.newField": oldField,
"system.-=oldField": null
});
}
}
}
if (updates.length > 0) {
await actor.updateEmbeddedDocuments("Item", updates);
console.log(`[MyModule] Migrated ${updates.length} items for ${actor.name}`);
}
}
static async migrate() {
const currentVersion = game.settings.get(MODULE_ID, "schemaVersion") || 0;
const targetVersion = 2;
if (currentVersion < targetVersion) {
ui.notifications.info("My Module: Migrating data, please wait...");
try {
for (const actor of game.actors) {
if (actor.type !== "character") continue;
// Wrap each migration in try-catch to prevent one failure from blocking others
try {
await this.migrateEquippedItems(actor);
await this.migrateAbilityProgress(actor);
} catch (err) {
console.error(`[MyModule] Migration failed for ${actor.name}:`, err);
ui.notifications.warn(`Migration failed for ${actor.name} - see console`);
}
}
// Update version even if some actors failed
await game.settings.set(MODULE_ID, "schemaVersion", targetVersion);
ui.notifications.info("My Module: Migration complete.");
} catch (err) {
console.error("[MyModule] Critical migration error:", err);
ui.notifications.error("My Module: Migration failed. See console for details.");
}
}
}
Error handling strategies:
// In browser console
const actor = game.actors.getName("Test Character");
// Set old system field
await actor.update({ "system.old-field": "test value" });
// Set old array-based flag
await actor.setFlag("my-module", "equipped-items", [{ id: "abc123" }]);
// Verify old data
console.log(actor.system["old-field"]); // "test value"
console.log(actor.getFlag("my-module", "equipped-items")); // [{ id: "abc123" }]
// Force schemaVersion back to 0
await game.settings.set("my-module", "schemaVersion", 0);
// Run migration
await Migration.migrate();
// Check results
const actor = game.actors.getName("Test Character");
console.log(actor.getFlag("my-module", "new_field")); // "test value"
console.log(actor.system["old-field"]); // undefined (removed)
Before committing migration code:
targetVersion in migrate() methodmigrate{FeatureName})actor.update() call-= syntax to remove old fieldsmigrate() sequencescripts/migration.jsscripts/settings.jsscripts/module.js (ready hook)CONTRIBUTING.md line 56For BitD Alternate Sheets module:
scripts/settings.js line 59scripts/module.js line 43 (ready hook)scripts/migration.js line 6character, crew, npcxiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
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...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
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...
Display project status, active tracks, and next actions
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.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience