Repository navigation
api tags
Audience: Developers integrating with Blacksmith and using the Tags system from another module.
Feature summary:
- Central storage: All tag assignments (record → tags) are stored in a Blacksmith world setting. Consuming modules do not store tags in their own record data.
-
Context keys: Every tag operation is scoped to a
{moduleId}.{dataType}context key (e.g."coffee-pub-librarian.quest"). This allows the same tag name to exist independently in different data types. -
Taxonomy:
resources/tag-taxonomy.jsondefines the suggested tag set for each context. Modules contribute by adding their context entries to this file. Protected tags (those that drive module code) are markedprotected: trueand cannot be renamed or deleted. -
Global tags: Tags like
"todo"and"revisit"are offered as suggestions in every context. - Visibility: Per-tag-globally by default, with an optional per-context override. Visibility is client-scope and affects filtering only — it does not remove tags from data.
- TagWidget: A reusable Blacksmith UI component for tag selection, search, add, and remove. Embed in any Application V2 window; requires the Blacksmith Window API.
- GM-only mutations: Rename and delete operate across all records in all contexts and require GM privileges.
The Tags API provides a shared labeling infrastructure for all coffee-pub modules. Use it to attach classification labels to your records, offer a consistent UI for choosing and filtering tags, and let the GM manage the world's tag vocabulary in one place.
A tag is a normalized string: lowercase, hyphen-separated, no spaces (e.g. "main-quest", "tavern", "todo"). A context key scopes a tag taxonomy and its record assignments to one module + data type (e.g. "coffee-pub-librarian.quest").
- You have records (quests, codex entries, notes, items) that users want to classify and filter.
- You want a consistent tag UI without building tag-input, chip rendering, or search from scratch.
- You want the GM to be able to rename or delete a tag world-wide and have it propagate to all records automatically.
- For boolean or typed metadata on a record (use your own data model). Tags are display labels, not typed configuration fields.
- For system-level classification that drives rendering or behavior and never changes (use your
typefield or constants instead — and if you do define such values in the taxonomy, mark themprotected: true).
| File | Role |
|---|---|
scripts/manager-tags.js |
TagManager — core logic, storage, normalization |
scripts/api-tags.js |
TagsAPI — public wrapper (this document) |
scripts/widget-tags.js |
TagWidget — embeddable UI component |
templates/partials/tag-widget.hbs |
TagWidget Handlebars template |
styles/widget-tags.css |
TagWidget styles |
resources/tag-taxonomy.json |
Canonical taxonomy for all coffee-pub contexts |
const tags = game.modules.get('coffee-pub-blacksmith')?.api?.tags;
if (!tags?.isAvailable()) return;Call isAvailable() before any API use. The Tags API is ready as soon as Blacksmith initializes — it does not require the canvas.
Add your context key entry to resources/tag-taxonomy.json before shipping:
{
"version": 1,
"globalTags": ["todo", "revisit", "avoid", "complete"],
"contexts": {
"coffee-pub-librarian.quest": {
"label": "Quests",
"tags": [
{ "key": "main", "protected": true },
{ "key": "side", "protected": true },
{ "key": "backstory" },
{ "key": "faction" }
]
}
}
}Mark protected: true on any tag your code checks by value (e.g. if (tags.includes('main'))). Protected tags cannot be renamed or deleted by a GM.
Hooks.on('blacksmithReady', async () => {
const tags = game.modules.get('coffee-pub-blacksmith')?.api?.tags;
if (!tags?.isAvailable()) return;
// Set tags on a record (replaces any existing tags for this record)
await tags.setTags('coffee-pub-librarian.quest', quest.id, ['main', 'faction']);
});await tags.deleteRecordTags('coffee-pub-librarian.quest', quest.id);See TagWidget below for the full embed pattern.
Returns true if the Tags API is loaded and ready. Safe to call at any time, including before ready.
Returns: boolean
const tags = game.modules.get('coffee-pub-blacksmith')?.api?.tags;
if (!tags?.isAvailable()) {
console.warn('Blacksmith Tags API not available');
return;
}Merge a taxonomy entry into the in-memory registry at runtime. Useful for dev-time testing or dynamic contexts not known at ship time. For shipped modules, prefer adding entries directly to tag-taxonomy.json.
Runtime entries merge on top of the JSON entries — runtime wins on key collision.
Parameters:
| Name | Type | Description |
|---|---|---|
contextKey |
string |
Context identifier in {moduleId}.{dataType} format |
taxonomy |
object |
{ label: string, tags: Array<string | { key: string, protected?: boolean }> } |
Use tags as the array key. flags is also accepted — the shipped tag-taxonomy.json uses it, and both readers take either key — but tags is the canonical shape. Entries may be plain strings or { key, protected } objects.
Returns: void
Hooks.on('blacksmithReady', () => {
const tags = game.modules.get('coffee-pub-blacksmith')?.api?.tags;
tags?.register('coffee-pub-librarian.quest', {
label: 'Quests',
tags: [
{ key: 'main', protected: true },
{ key: 'side', protected: true },
{ key: 'backstory' },
{ key: 'faction' }
]
});
});Get the full list of suggested tags for a context: the context's taxonomy tags plus global tags. Use this to populate a dropdown, chip input, or TagWidget's suggestion list.
Tags are returned in taxonomy order, with global tags appended. Protected tags are included; the caller can use the protected field to style or sort them differently.
Parameters:
| Name | Type | Description |
|---|---|---|
contextKey |
string |
Context identifier |
Returns: Array<{ key: string, label: string, protected: boolean, tier: 'taxonomy' \| 'global' }>
const choices = tags.getChoices('coffee-pub-librarian.quest');
// [
// { key: 'main', label: 'Main', protected: true, tier: 'taxonomy' },
// { key: 'side', label: 'Side', protected: true, tier: 'taxonomy' },
// { key: 'backstory', label: 'Backstory', protected: false, tier: 'taxonomy' },
// { key: 'todo', label: 'Todo', protected: false, tier: 'global' },
// ]All CRUD methods write to the central tagAssignments world setting. Non-GM users go through the GM proxy automatically — you do not need to handle that routing.
Replace the tag set for a record. Normalizes the input array before storing. Adds any new tags to the world registry.
Parameters:
| Name | Type | Description |
|---|---|---|
contextKey |
string |
Context identifier |
recordId |
string |
Unique identifier of the record within this context |
tagArray |
string[] |
The complete new tag set (replaces existing) |
Returns: Promise<void>
// On quest save
await tags.setTags('coffee-pub-librarian.quest', quest.id, ['main', 'faction', 'todo']);Get the current tags for a record. Returns an empty array if the record has no tags.
Parameters:
| Name | Type | Description |
|---|---|---|
contextKey |
string |
Context identifier |
recordId |
string |
Record identifier |
Returns: string[]
const questFlags = tags.getTags('coffee-pub-librarian.quest', quest.id);
// ['main', 'faction', 'todo']Add tags to a record without replacing its existing tags. Duplicates are deduplicated automatically. Adds any new tags to the world registry.
Parameters:
| Name | Type | Description |
|---|---|---|
contextKey |
string |
Context identifier |
recordId |
string |
Record identifier |
tagArray |
string[] |
Tags to add |
Returns: Promise<void>
// Mark a quest complete without touching its other tags
await tags.addTags('coffee-pub-librarian.quest', quest.id, ['complete']);Remove specific tags from a record. Tags not present on the record are silently ignored.
Parameters:
| Name | Type | Description |
|---|---|---|
contextKey |
string |
Context identifier |
recordId |
string |
Record identifier |
tagArray |
string[] |
Tags to remove |
Returns: Promise<void>
// Un-complete a quest
await tags.removeTags('coffee-pub-librarian.quest', quest.id, ['complete']);Remove all tag data for a record. Call this when the record itself is deleted to avoid orphan entries in the assignment store.
Parameters:
| Name | Type | Description |
|---|---|---|
contextKey |
string |
Context identifier |
recordId |
string |
Record identifier |
Returns: Promise<void>
// In your module's record delete handler
await tags.deleteRecordTags('coffee-pub-librarian.quest', deletedQuest.id);How many records in one context carry each tag. One pass over that context.
| Parameter | Type | Description |
|---|---|---|
contextKey |
string |
The context to count within |
Returns Record<string, number> -- tag to record count. An unknown context returns {}.
Only tags in use appear. A taxonomy tag nobody has applied is absent, not present with 0: this
reports assignments, while getChoices reports suggestions. Treating an unused suggestion as a stale
entry is the mistake this distinction exists to prevent.
Use it to scope a vocabulary to your own records -- getRegistry() is world-wide and undifferentiated,
so sizing a tag cloud from it means one getRecordsByTag call per registry entry.
Whether a registry entry is accounted for anywhere is a different question, needing every context key
and the declared taxonomy across all of them. That is not on this surface: no consumer can assemble it
without hardcoding keys it does not own. Blacksmith answers it in utilities/audit-tag-registry.js.
Get all record IDs in a context that currently have a specific tag. Useful for building filter views.
Parameters:
| Name | Type | Description |
|---|---|---|
contextKey |
string |
Context identifier |
tag |
string |
Tag to look up |
Returns: string[] — array of record IDs
// Find all quests tagged 'faction'
const factionQuestIds = tags.getRecordsByTag('coffee-pub-librarian.quest', 'faction');These methods operate on the world-level tag registry. Rename and delete require GM privileges and propagate across all contexts and records automatically.
Get the full world tag list — every tag ever used in any context, including custom and orphan tags.
Returns: string[] — sorted array of normalized tag strings
const allFlags = tags.getRegistry();
// ['avoid', 'backstory', 'complete', 'faction', 'main', 'revisit', 'tavern', 'todo', ...]Normalize an array of tag strings: lowercase, replace spaces with hyphens, deduplicate. Use this before storing tags you receive from user input.
Parameters:
| Name | Type | Description |
|---|---|---|
tagArray |
string[] |
Raw input tags |
Returns: string[] — normalized, deduplicated
const clean = tags.normalize(['Main Quest', 'FACTION', 'main quest']);
// ['main-quest', 'faction']Rename a tag globally. Updates every record in every context that uses oldTag, then updates the registry. Silently rejected if oldTag is protected.
GM only. Non-GM callers receive a warning and no change is made.
Parameters:
| Name | Type | Description |
|---|---|---|
oldTag |
string |
Existing tag key |
newTag |
string |
Replacement tag key (will be normalized) |
Returns: Promise<{ updated: number } \| null> — number of records updated, or null if rejected
// Rename 'backstory' → 'lore' across all contexts and records
const result = await tags.rename('backstory', 'lore');
// { updated: 14 }Delete a tag globally. Removes it from every record in every context, then removes it from the registry. Silently rejected if the tag is protected.
GM only. Non-GM callers receive a warning and no change is made.
Parameters:
| Name | Type | Description |
|---|---|---|
tag |
string |
Tag key to delete |
Returns: Promise<{ removed: number } \| null> — number of records affected, or null if rejected
const result = await tags.delete('old-unused-tag');
// { removed: 3 }Populate the world registry from a flat list of tag arrays. Use this once during your module's first-run initialization to ensure the registry reflects all tags already present in your existing data — before any user interaction has created them through normal use.
If the registry already contains a tag it is not duplicated.
Parameters:
| Name | Type | Description |
|---|---|---|
contextKey |
string |
Context identifier (used for logging only) |
existingTagArrays |
string[][] |
Array of tag arrays, one per record |
Returns: Promise<void>
// On first load: seed registry from all existing quests
const allQuestFlags = myQuests.map(q => tags.getTags('coffee-pub-librarian.quest', q.id));
await tags.seedRegistry('coffee-pub-librarian.quest', allQuestFlags);Visibility is client-scope (per-user) and affects UI filtering only. It does not remove tags from stored data or prevent other users from seeing them.
Resolution order: Context override → global default → true (visible if no entry exists).
Set the visibility of a tag. If contextKey is provided, this sets a context-specific override. If omitted, it sets the global default for that tag across all contexts.
Parameters:
| Name | Type | Description |
|---|---|---|
tag |
string |
Tag key |
visible |
boolean |
Whether the tag should be visible in filter UIs |
contextKey |
string? |
Optional — scope override to one context |
Returns: void
// Hide 'todo' globally (across all contexts for this user)
tags.setVisibility('todo', false);
// Hide 'backstory' only in the quests context
tags.setVisibility('backstory', false, 'coffee-pub-librarian.quest');
// Restore 'backstory' visibility in quests (removes the context override)
tags.setVisibility('backstory', true, 'coffee-pub-librarian.quest');Get the effective visibility of a tag for the current user. Applies context override → global default → true fallback.
Parameters:
| Name | Type | Description |
|---|---|---|
tag |
string |
Tag key |
contextKey |
string? |
Optional — check effective visibility for a specific context |
Returns: boolean
// Is 'todo' visible in the quests context?
const visible = tags.getVisibility('todo', 'coffee-pub-librarian.quest');TagWidget is a Blacksmith UI component for selecting and managing tags. Embed it in any Application V2 window instead of building your own tag input.
Prerequisite: Your window must use the Blacksmith Window API (documentation/api/api-window.md). TagWidget follows the same lifecycle and CSS conventions.
| Mode | Supports |
|---|---|
| Full (default) | Display existing tags as removable chips, input with live search against suggestions, add custom tags, remove tags |
| Filter |
Not implemented. prepareData({mode:'filter'}) and the template render the toggles, but nothing listens to them — there is no change handler, so toggling never calls setVisibility. Don't use it. |
Four steps, and step 3 is not optional — without activate() the widget renders and does nothing.
1. In prepareContext() — build widget data:
async prepareContext(options) {
const context = await super.prepareContext(options);
const tags = game.modules.get('coffee-pub-blacksmith')?.api?.tags;
const currentTags = tags.getTags('coffee-pub-librarian.quest', this.questId);
context.TagWidget = TagWidget.prepareData({
contextKey: 'coffee-pub-librarian.quest',
currentTags,
mode: 'full'
});
return context;
}2. In your Handlebars template — include the partial:
<div class="quest-tags-section">
{{> blacksmith-tag-widget TagWidget}}
</div>Pass the context positionally, not as a hash. {{> blacksmith-tag-widget tags=TagWidget}} does
not work: a hash argument adds tags to your context rather than replacing it, and the partial reads
contextKey / isFullMode / chips off the root context. You get a silent empty div.
3. After render — wire up interactivity:
_onRender(context, options) {
super._onRender(context, options);
TagWidget.activate(this.element, 'coffee-pub-librarian.quest');
}Without this the widget is inert. activate() is the entire event layer — suggestion clicks,
Enter-to-add, chip removal, live search. Rendering the partial alone gives you a display-only div.
4. On save — read the widget value and persist:
async _onSubmit(event, form, formData) {
const tags = game.modules.get('coffee-pub-blacksmith')?.api?.tags;
const newTags = TagWidget.readValue(this.element, 'coffee-pub-librarian.quest');
await tags.setTags('coffee-pub-librarian.quest', this.questId, newTags);
// ... rest of save
}const { TagWidget } = game.modules.get('coffee-pub-blacksmith')?.api?.tags ?? {};Prepare the template data object for the widget partial.
Parameters:
| Name | Type | Description |
|---|---|---|
options.contextKey |
string |
Context identifier |
options.currentTags |
string[] |
Tags currently on the record |
options.mode |
'full' | 'filter' |
Widget mode (default: 'full'). 'filter' is not implemented — see Capabilities. |
options.placeholder |
string? |
Input placeholder text (full mode only) |
Returns: object — template data. Pass it positionally: {{> blacksmith-tag-widget TagWidget}}.
Required. Wires the widget's event handlers — suggestion clicks, Enter-to-add, chip removal, live
search. Call it after every render (e.g. in _onRender). Without it the widget renders but does nothing.
Parameters:
| Name | Type | Description |
|---|---|---|
element |
HTMLElement |
The window's root element (e.g. this.element) |
contextKey |
string |
Context identifier (must match prepareData) |
onChange |
Function? |
Optional callback fired when the selection changes |
Read the current tag selection from a rendered widget inside element. Call this in your _onSubmit or change handler.
Parameters:
| Name | Type | Description |
|---|---|---|
element |
HTMLElement |
The window's root element (e.g. this.element) |
contextKey |
string |
Context identifier (must match the one used in prepareData) |
Returns: string[] — normalized array of selected tags
The Tags system fires the following Foundry hooks:
| Hook | When | Payload |
|---|---|---|
blacksmith.tags.registered |
A taxonomy is registered (JSON load or runtime) | { contextKey, taxonomy } |
blacksmith.tags.renamed |
A tag is renamed globally (GM only) | { oldTag, newTag, updated: number } |
blacksmith.tags.deleted |
A tag is deleted globally (GM only) | { tag, removed: number } |
blacksmith.tags.changed |
A record's tags actually changed, deleteRecordTags included -- it reports tags: []. A call that changes nothing is silent: addTags with a tag the record already carries, removeTags with one it does not, deleteRecordTags on an absent record. Fires on the calling client only -- no other client is notified. |
{ contextKey, recordId, tags: string[] } |
When adding Tags support to a new module or data type:
- Add your context key to
resources/tag-taxonomy.json. Markprotected: trueon any tags your code checks by value. - Call
tags.setTags(contextKey, recordId, tagArray)when saving a record. - Call
tags.deleteRecordTags(contextKey, recordId)when deleting a record. - Call
tags.seedRegistry(contextKey, existingArrays)once on first load to populate the registry from pre-existing data. - Embed
TagWidgetin your record's edit window. - Use
tags.getRecordsByTag(contextKey, tag)to power any filter UI. - Check
tags.isAvailable()before all API calls from outside Blacksmith.
Tags are not appearing in suggestions
- Verify your context key is in
tag-taxonomy.json(or registered at runtime before the UI opens). - Check that
tags.getChoices(contextKey)returns your expected tags in the console.
Rename / delete silently does nothing
- Confirm the calling user is GM.
- Check if the tag is marked
protected: truein the taxonomy — protected tags cannot be renamed or deleted.
TagWidget is not rendering
- Confirm your window extends the Blacksmith Window API (see
documentation/api/api-window.md). - Confirm you are passing the
TagWidgetvalue fromprepareData()to the template context, not constructing the object manually.
Record tags are empty after reload
- Verify you are calling
setTags(not just updating your own record data). - Check the
tagAssignmentsworld setting in the browser console:game.settings.get('coffee-pub-blacksmith', 'tagAssignments').
- Ai assistance
- Applicationv2 traps
- CSS traps
- dnd5e conditions
- Documentation standard
- Token actor identity
- Campaign
- Canvas
- Chatcards
- Compendiums
- Contextmenu
- Core
- Create journal entry
- Dialog
- Active Effects
- Entity list
- Equiplocations
- Geography
- GM request
- Gmnotes
- Health
- Hookmanager
- Importer
- Inventory
- Menubar
- Notes
- Party
- Pins
- Quantity split
- Requestroll
- Rolls
- Scene config
- Sockets
- Stats
- Tags
- Toast
- Tokens
- Toolbar
- Window
- Worldclock