This skill should be used when creating custom windows with ApplicationV2, building forms with FormApplication, using Dialog for prompts, understanding the render lifecycle, or migrating from Application v1 to ApplicationV2.
Build custom windows and dialogs using ApplicationV2 (V12+) and DialogV2. Use when creating forms, prompts, or migrating from deprecated Application v1.
/plugin marketplace add ImproperSubset/hh-agentics/plugin install fvtt-dev@hh-agenticsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Domain: Foundry VTT Module/System Development Status: Production-Ready Last Updated: 2026-01-05
Applications are the window/dialog system in Foundry. ApplicationV2 (V12+) is the modern framework replacing the legacy Application v1 (deprecated, removed in V16).
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
class MyWindow extends HandlebarsApplicationMixin(ApplicationV2) {
static DEFAULT_OPTIONS = {
id: "my-window",
classes: ["my-module"],
position: { width: 400, height: "auto" },
window: {
title: "My Window",
icon: "fas fa-gear"
}
};
static PARTS = {
main: {
template: "modules/my-module/templates/window.hbs"
}
};
async _prepareContext(options) {
return {
message: "Hello World"
};
}
}
// Usage
new MyWindow().render(true);
class MyForm extends HandlebarsApplicationMixin(ApplicationV2) {
static DEFAULT_OPTIONS = {
id: "my-form",
tag: "form", // CRITICAL for form handling
window: { title: "Settings" },
position: { width: 500 },
form: {
handler: MyForm.#onSubmit,
submitOnChange: false,
closeOnSubmit: true
}
};
static PARTS = {
form: {
template: "modules/my-module/templates/form.hbs"
}
};
async _prepareContext() {
return {
settings: this.settings
};
}
static async #onSubmit(event, form, formData) {
const data = foundry.utils.expandObject(formData.object);
console.log("Submitted:", data);
// Process form data
}
}
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: { title: "Confirm" },
content: "<p>Delete this item?</p>"
});
if (confirmed) {
await item.delete();
}
const name = await foundry.applications.api.DialogV2.prompt({
window: { title: "Enter Name" },
content: "<p>What is your character's name?</p>",
label: "Submit"
});
const result = await foundry.applications.api.DialogV2.wait({
window: { title: "Choose Action" },
content: "<p>What would you like to do?</p>",
buttons: [{
icon: "fas fa-check",
label: "Accept",
action: "accept"
}, {
icon: "fas fa-times",
label: "Decline",
action: "decline"
}]
});
if (result === "accept") {
// Handle accept
}
const data = await foundry.applications.api.DialogV2.prompt({
window: { title: "Configure" },
content: `
<div class="form-group">
<label>Name</label>
<input type="text" name="name" value="">
</div>
<div class="form-group">
<label>Level</label>
<input type="number" name="level" value="1">
</div>
`,
ok: {
callback: (event, button, dialog) => {
const form = dialog.querySelector("form");
return new FormDataExtended(form).object;
}
}
});
class MyApp extends HandlebarsApplicationMixin(ApplicationV2) {
// Prepare data for template
async _prepareContext(options) {
return { items: this.items };
}
// Prepare data for specific part
async _preparePartContext(partId, context) {
if (partId === "list") {
context.sortedItems = this.sortItems(context.items);
}
return context;
}
// After first render only
async _onFirstRender(context, options) {
this.setupInitialState();
}
// After every render
async _onRender(context, options) {
this.attachEventListeners();
}
}
render(true) called
→ _preRender()
→ _prepareContext()
→ _preparePartContext() (per part)
→ Template rendering
→ _onFirstRender() (first time only)
→ _onRender()
→ Hook: renderMyApp
class MyApp extends HandlebarsApplicationMixin(ApplicationV2) {
static DEFAULT_OPTIONS = {
actions: {
delete: MyApp.#onDelete,
edit: MyApp.#onEdit
}
};
static async #onDelete(event, target) {
const itemId = target.dataset.itemId;
await this.deleteItem(itemId);
}
static #onEdit(event, target) {
const itemId = target.dataset.itemId;
this.editItem(itemId);
}
}
Template:
<button type="button" data-action="delete" data-item-id="{{item.id}}">
Delete
</button>
async _onRender(context, options) {
this.element.querySelector(".custom-button")
?.addEventListener("click", this._onCustomClick.bind(this));
}
_onCustomClick(event) {
event.preventDefault();
// Handle click
}
static PARTS = {
header: {
template: "modules/my-mod/templates/header.hbs"
},
tabs: {
template: "templates/generic/tab-navigation.hbs"
},
content: {
template: "modules/my-mod/templates/content.hbs",
scrollable: [""] // Enable scroll preservation
},
footer: {
template: "modules/my-mod/templates/footer.hbs"
}
};
static PARTS = {
tabs: { template: "templates/generic/tab-navigation.hbs" },
details: { template: "templates/details.hbs" },
inventory: { template: "templates/inventory.hbs" }
};
static TABS = {
primary: {
tabs: [
{ id: "details", label: "Details" },
{ id: "inventory", label: "Inventory" }
],
initial: "details"
}
};
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.tabs = this._prepareTabs();
return context;
}
async _preparePartContext(partId, context) {
if (["details", "inventory"].includes(partId)) {
context.tab = context.tabs[partId];
}
return context;
}
For maintenance of existing code only:
class LegacyForm extends FormApplication {
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: "legacy-form",
title: "Legacy Form",
template: "modules/my-mod/templates/form.hbs",
width: 400,
closeOnSubmit: true
});
}
getData() {
return { data: this.object };
}
async _updateObject(event, formData) {
await this.object.update(formData);
}
activateListeners(html) {
super.activateListeners(html);
html.find(".button").click(this._onClick.bind(this));
}
}
// WRONG - form submission won't work
static DEFAULT_OPTIONS = {
form: { handler: MyApp.#onSubmit }
};
// CORRECT
static DEFAULT_OPTIONS = {
tag: "form",
form: { handler: MyApp.#onSubmit }
};
<!-- WRONG - triggers form submission -->
<button>Click Me</button>
<!-- CORRECT - won't submit form -->
<button type="button">Click Me</button>
// WRONG - DialogV2 cannot re-render
const dialog = new DialogV2({...});
await dialog.render(true);
await dialog.render(true); // Error!
// CORRECT - use ApplicationV2 for re-renderable windows
class MyDialog extends HandlebarsApplicationMixin(ApplicationV2) {}
// Always await async operations
async _prepareContext(options) {
const data = await this.loadData(); // OK
return { data };
}
// WRONG - loses context
this.element.addEventListener("click", this._onClick);
// CORRECT - bind context
this.element.addEventListener("click", this._onClick.bind(this));
// Or use arrow function
this.element.addEventListener("click", (e) => this._onClick(e));
{{!-- Use standard-form class --}}
<div class="standard-form">
<div class="form-group">
<label>Field</label>
<input type="text" name="field">
</div>
</div>
HandlebarsApplicationMixin(ApplicationV2)static DEFAULT_OPTIONS with id, classes, positionstatic PARTS for templates_prepareContext() for template datatag: "form" for form applicationsform.handler for form submissiondata-action attributes with static actionstype="button" on non-submit buttonsDialogV2.confirm() for yes/no promptsDialogV2.prompt() for text inputDialogV2.wait() for custom buttonsLast Updated: 2026-01-05 Status: Production-Ready Maintainer: ImproperSubset
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.