CraftingSystemManager

Manages crafting system configurations and their component libraries.

Access: game.fabricate.getCraftingSystemManager()


System Methods

getSystems()

Returns all crafting systems.

Returns: object[]

getSystem(systemId)

Returns a single system by ID.

Returns: object | null

createSystem(data)

Creates a new crafting system. GM only.

Parameter Type Description
data object System configuration (name, description, resolutionMode, features, essenceDefinitions, etc.)

Returns: Promise<object>

The features object controls which optional behaviours are active. Every key defaults to false when omitted, except salvage, which defaults to true (see the table below).

Feature key Type Default Description
recipeCategories boolean false Organise recipes into named categories
itemTags boolean false Tag-based ingredient matching
essences boolean false Enable the essences system
propertyMacros boolean false Allow result property macros
effectTransfer boolean false Copy active effects from ingredients to crafted results. Also requires recipe.transferEffects: true on each recipe. See Effect Transfer.
multiStepRecipes boolean false Multi-step recipes
salvage boolean true Allow components to be broken down into constituent parts. Defaults on for backward compatibility; an explicit false is honoured. When enabled (the default), each normalised component gains a salvage sub-object. See Salvage.
gathering boolean false Enable GM authoring for gathering environments and tasks. See Gathering Environments.
const mgr = game.fabricate.getCraftingSystemManager();
const system = await mgr.createSystem({
  name: 'Alchemy',
  description: 'Brew potions and elixirs',
  resolutionMode: 'simple',
  features: {
    recipeCategories: true,
    essences: true,
    effectTransfer: true,
    multiStepRecipes: false,
    salvage: false // opt out; salvage is on by default
  },
  essenceDefinitions: [
    {
      name: 'Fire',
      description: 'The raw energy of flame',
      icon: 'fas fa-fire',
      sourceItemUuid: null
    },
    {
      name: 'Frost',
      description: 'The biting cold of winter',
      icon: 'fas fa-snowflake',
      sourceItemUuid: null
    }
  ]
});
console.log(`Created system: ${system.id}`);

The returned system object also includes the following top-level salvage fields, which are always normalised regardless of whether the salvage feature is enabled:

Field Type Default Description
salvageResolutionMode string "simple" How salvage result groups are selected. Accepts "simple", "routed", or "progressive". Legacy "tiered" input is normalized to "routed". "mapped" and "alchemy" are rejected and fall back to "simple".
salvageCraftingCheck object see below System-level salvage check configuration.

salvageCraftingCheck shape:

Field Type Default Description
enabled boolean false Whether the optional simple-mode salvage check is active.
simple object see below Simple pass/fail salvage check (simple salvage mode). Usable only when simple.rollFormula is set.
routed object see below Routed salvage check (routed salvage mode). Usable only when routed.rollFormula is set.
consumption.consumeComponentOnFail boolean true Consume the source component even when the check fails
consumption.breakToolsOnFail boolean false Break Tools even when the salvage check fails (renamed from the legacy consumeCatalystsOnFail, which is still read as a fallback)
progressive.rollFormula string "" Roll formula for the progressive salvage check (progressive salvage mode). The check is usable only when this is set.
progressive.awardMode string "equal" Progressive award mode: "equal", "exceed", or "partial"
outcomes string[] ["fail","pass"] Named outcome labels used for routed check routing

The craftingCheck field is always present on the returned system object. It controls how skill/ability checks gate recipe outcomes in routed-by-check and progressive modes.

craftingCheck shape:

Field Type Default Description
enabled boolean false Whether the optional simple-mode crafting check is active. This toggle gates the simple pass/fail check in simple mode only.
simple.rollFormula string "" Roll formula for the simple pass/fail check (simple mode, and alchemy when alchemy.checkMode is "simple"). The check is usable only when this is set.
simple.dc number 15 Static difficulty class for the simple check. Roll total must meet or exceed it (or strictly exceed it when simple.thresholdMode is "exceed").
simple.dcMode string "static" "static" uses simple.dc. "dynamic" computes the DC from the macro at simple.macroUuid.
simple.macroUuid string\|null null UUID of the dynamic-DC macro, used only when simple.dcMode is "dynamic". The macro computes the DC. It never resolves the check outcome.
routed.rollFormula string "" Roll formula for the routed crafting check. Required in routedByCheck mode and in alchemy when alchemy.checkMode is "tiered", and optional in routedByIngredients mode. The check is usable only when this is set.
progressive.rollFormula string "" Roll formula for the progressive check (progressive mode). The check is usable only when this is set.
consumption.consumeIngredientsOnFail boolean true Remove ingredients from inventory when the check fails.
consumption.breakToolsOnFail boolean false Break Tools when the crafting check fails (renamed from the legacy consumeCatalystsOnFail, which is still read as a fallback).
progressive.awardMode string "equal" Progressive award mode: "equal", "exceed", or "partial".
outcomes string[] ["fail","pass"] Named outcome labels used for routed check routing.

progressive.allowPlayerReorder is retired (issue 651) and is dropped from the crafting, salvage, and gathering progressive check blocks on every normalise, including on import of a legacy payload. The 1.17.0 migration strips it from stored systems. The permission it carried now lives on the recipe as allowPlayerResultReorder and on the component as salvage.allowPlayerResultReorder, and it defaults to true where it previously defaulted to false. Gathering never had an ordered result-stage surface, so it has no replacement field.

The alchemy field is present only when resolutionMode is "alchemy". It carries the alchemy check mode and the discovery/consumption options.

alchemy shape:

Field Type Default Description
checkMode string "none" How a matched brew is resolved. "none" runs no check and always succeeds. "simple" runs a mandatory pass/fail check from craftingCheck.simple (pass produces the success group, fail produces the reserved role: 'failure' group). "tiered" runs a mandatory routed check from craftingCheck.routed and routes by outcome tier, exactly like routedByCheck.
learnOnCraft boolean true Mark a recipe as learned for the crafting character when a submission matches it. A match learns the recipe whether or not the check passed, and whether the brew resolves immediately or after a time gate. Under global visibility this is the only source of discovery, so turning it off leaves every player’s Known list permanently empty.
consumeOnFail boolean true Consume the submitted components on a no-match fizzle and on a matched Simple-check failure.
showAttemptHistoryToPlayers boolean true Remember a character’s fizzled combinations so the workbench can mark them as a dead end rather than an untried mix.

Example: routed-by-check. Configure a routedByCheck system to roll a check and route the result by outcome name:

Hooks.once('fabricate.ready', async () => {
  const mgr = game.fabricate.getCraftingSystemManager();
  await mgr.updateSystem('alchemy-system-id', {
    resolutionMode: 'routedByCheck',
    craftingCheck: {
      routed: {
        rollFormula: '1d20 + @abilities.int.mod',
        dc: 18
      },
      outcomes: ['fail', 'pass']
    }
  });
});

Example: optional simple check. Enable an optional pass/fail check in simple mode by setting a roll formula and turning the check on:

Hooks.once('fabricate.ready', async () => {
  const mgr = game.fabricate.getCraftingSystemManager();
  await mgr.updateSystem('alchemy-system-id', {
    craftingCheck: {
      enabled: true,
      simple: {
        rollFormula: '1d20 + @abilities.int.mod',
        dc: 15
      }
    }
  });
});

Example: alchemy Simple check. Give an alchemy system a mandatory pass/fail check so a matched brew rolls, and produces the reserved failure result group on a fail:

Hooks.once('fabricate.ready', async () => {
  const mgr = game.fabricate.getCraftingSystemManager();
  await mgr.updateSystem('alchemy-system-id', {
    alchemy: { checkMode: 'simple' },
    craftingCheck: {
      simple: {
        rollFormula: '1d20 + @abilities.int.mod',
        dc: 15
      }
    }
  });
});

updateSystem(systemId, updates)

Updates a system’s configuration. GM only.

Parameter Type Description
systemId string System ID
updates object Partial system data to merge

Returns: Promise<object>

When updating features, only the keys you provide are changed. Other feature flags are preserved from the existing system.

// Enable effect transfer on an existing system without changing other features.
Hooks.once('fabricate.ready', async () => {
  const mgr = game.fabricate.getCraftingSystemManager();
  await mgr.updateSystem('alchemy-system-id', {
    features: { effectTransfer: true }
  });
});
// Enable salvage with routed mode on an existing system.
Hooks.once('fabricate.ready', async () => {
  const mgr = game.fabricate.getCraftingSystemManager();
  await mgr.updateSystem('blacksmithing-system-id', {
    features: { salvage: true },
    salvageResolutionMode: 'routed',
    salvageCraftingCheck: {
      routed: {
        rollFormula: '1d20 + @abilities.int.mod',
        dc: 18
      },
      consumption: {
        consumeComponentOnFail: true,
        breakToolsOnFail: false
      },
      outcomes: ['critical', 'pass', 'fail']
    }
  });
});

Changing resolutionMode is destructive and deletes all recipes in the system.

deleteSystem(systemId)

Deletes a system and all its recipes. GM only.

Returns: Promise<void>

Deleting a system emits one summary notification naming the crafting system and counting related entities deleted with it. It does not emit one notification per deleted recipe.

Deletion is resilient to a recipe that cannot be removed. If an individual recipe deletion fails, that failure is logged to the console with the recipe id, the remaining recipes are still deleted, and the system itself is still removed. In that case the summary is a warning that also reports how many recipes could not be auto-deleted and may need manual removal.


Component Methods

Returns components for a system, optionally filtered by search text.

Parameter Type Description
systemId string System ID
search string Optional search filter

Returns: object[]

The system object exposes components under two equivalent properties: components (primary) and managedItems (transitional alias). Both always refer to the same array.

addItemFromUuid(systemId, itemUuid)

Adds a single Foundry Item document to the system as a component. GM only.

Returns a result object that indicates whether the item was newly created, updated, or already up to date, so callers can show appropriate notifications.

The method resolves both the dropped item’s live UUID and its canonical source UUID (via _stats.compendiumSource, with flags.core.sourceId as a legacy fallback) before deciding what to do. If the canonical source UUID no longer resolves, Fabricate stores the live dropped item UUID as the component’s primary source and keeps the broken canonical UUID in aliasItemUuids. A component can claim a full source-reference chain through registeredItemUuid, originItemUuid, and aliasItemUuids.

  1. Claimed source chain. An existing component already claims either the dropped live UUID, the canonical source UUID, or a fallback UUID in the same chain. Fabricate refreshes the component in place and returns action: "updated" when metadata or stored references changed, or action: "skipped" when nothing changed.
  2. Unclaimed source chain. No component claims any of those references, so a new component is created and action is "added".
Parameter Type Description
systemId string System ID
itemUuid string UUID of the Foundry item to add. Accepts both world item UUIDs (Item.abc123) and compendium item UUIDs (Compendium.pack.id.itemId).

Returns: Promise<{ item: object, action: 'added' | 'updated' | 'skipped', sourceFallbacks: object[] }>

  • item is the component object (new or existing).
  • action is "added" if a new component was created, "updated" if an existing component’s name/image/source references were refreshed, "skipped" if the claimed source chain was already current.
  • sourceFallbacks holds broken source-link fallback notices in the form { itemName, brokenUuid, fallbackUuid }. It is empty when no fallback occurred.

Throws: Error if the system ID is not found, or if the UUID resolves to a non-Item document (such as an Actor or JournalEntry).

Hooks.once('fabricate.ready', async () => {
  const mgr = game.fabricate.getCraftingSystemManager();
  const result = await mgr.addItemFromUuid(
    'alchemy-system-id',
    'Compendium.dnd5e.items.moonpetalHerb123'
  );
  if (result.action === 'added') {
    console.log(`Added: ${result.item.name} (componentId: ${result.item.id})`);
  } else if (result.action === 'updated') {
    console.log(`Updated metadata for: ${result.item.name}`);
  } else {
    console.log(`Already registered: ${result.item.name} — no changes needed.`);
  }
});

addItemsFromPack(systemId, packId)

Imports all Item documents from a compendium pack into the system as components. GM only.

Each item is processed via addItemFromUuid(), so the same source-chain deduplication rules apply: items already registered by the same live UUID or canonical source UUID are updated or skipped in place, and only unclaimed source chains create new components.

Parameter Type Description
systemId string System ID
packId string Compendium pack identifier in "scope.name" format (e.g. "dnd5e.items")

Returns: Promise<{ added: number, updated: number, skipped: number, total: number, sourceFallbacks: object[] }>

  • added is the number of items created as new components on this call.
  • updated is the number of items already registered whose name, image, or description was refreshed from the source.
  • skipped is the number of items already registered and already up to date, with no changes written.
  • total is the total number of Item documents found in the pack.
  • sourceFallbacks holds aggregated broken source-link fallback notices from imported items.

Throws: Error if the system ID or pack ID is not found.

Hooks.once('fabricate.ready', async () => {
  const mgr = game.fabricate.getCraftingSystemManager();
  const result = await mgr.addItemsFromPack(
    'herbalism-system-id',
    'world.herbs-and-reagents'
  );
  console.log(
    `Imported ${result.added} new, updated ${result.updated}, ` +
    `skipped ${result.skipped} of ${result.total} items.`
  );
});

You can also trigger bulk import from the UI by dragging a compendium pack header onto the Items tab drop zone in the Crafting Admin panel. The same deduplication logic applies. See Bulk compendium pack drop for details.

addToolFromUuid(systemId, itemUuid)

Registers a single Foundry Item document directly as a first-class Tool in the system’s Tools library (issue 561). GM only.

The Item does not need to be imported as a component first. The new tool carries its own source references and a name/image display snapshot captured from the Item, and componentId is null. The method stamps the durable tool identity flag (flags.fabricate.roles[systemId].toolId) on the source Item so future copies are recognised. An Item that is already a managed component can also be registered as a tool this way, in which case the Item carries both the component and tool role flags.

Parameter Type Description
systemId string System ID
itemUuid string UUID of the Foundry item to register as a tool

Returns: Promise<{ item: object, action: 'added' }>

Throws: Error if the system ID is not found, or if the UUID resolves to a non-Item document.

Hooks.once('fabricate.ready', async () => {
  const mgr = game.fabricate.getCraftingSystemManager();
  const result = await mgr.addToolFromUuid(
    'blacksmithing-system-id',
    'Item.hammer123'
  );
  console.log(`Registered tool: ${result.item.name} (toolId: ${result.item.id})`);
});

updateItem(systemId, itemId, updates)

Updates a component’s properties (category, tags, essences, difficulty, salvage). GM only.

Parameter Type Description
systemId string System ID
itemId string Component ID
updates object Partial item data

Returns: Promise<object>

If updates changes registeredItemUuid, originItemUuid, or aliasItemUuids, the manager enforces the same per-system uniqueness rule used by imports. An update that would make two components claim the same source-reference chain throws an Error.

updates is shallow-merged over the stored component, so a field you omit is preserved. Note this merge is shallow: updates.salvage REPLACES the whole stored salvage sub-object rather than merging into it, so a partial patch such as { salvage: { enabled: true } } drops resultGroups. Spread the existing sub-object when patching one of its keys.

category

category is the component’s single-valued grouping axis, and it drives grouping and filtering in the GM component browser. It is normalised to a non-empty string on every write. Any unusable value (a non-string, an empty string, or an absent key) reads as the reserved 'general' bucket, so every component has a category and there is no uncategorised state. This default is applied by normalisation rather than by a migration.

category is distinct from tags, which is many-valued and is not a grouping axis.

The system’s authored vocabulary lives in the top-level componentCategories array on the system. It is a sibling of the recipe categories array and is deliberately independent of it: the two vocabularies are never merged or cross-populated, so a component category is never offered as a recipe category. The reserved 'general' bucket is implied and is never stored in componentCategories. A category naming no entry in the vocabulary is still stored and shown verbatim.

// Categorise a component. Existing fields are preserved by the shallow merge.
Hooks.once('fabricate.ready', async () => {
  const mgr = game.fabricate.getCraftingSystemManager();
  await mgr.updateSystem('dragoncraft-system-id', { componentCategories: ['Reagent', 'Scale'] });
  await mgr.updateItem('dragoncraft-system-id', 'dragon-scale-component-id', {
    category: 'Scale'
  });
});

salvage

When features.salvage is enabled on the system, you can set the salvage sub-object here. The shape is normalised on write. See Component Salvage for the GM-facing walkthrough.

salvage.enabled gates salvageability per component and defaults to false. It is clamped to false whenever the normalised salvage.resultGroups is empty, because a component that is salvageable but yields nothing is a forbidden state. The clamp only ever turns enabled off, so it never enables salvage on a component that did not ask for it. This runs on every write, including imports, so { enabled: true, resultGroups: [] } normalises to { enabled: false, resultGroups: [] }.

// Configure salvage for a Dragon Scale component
Hooks.once('fabricate.ready', async () => {
  const mgr = game.fabricate.getCraftingSystemManager();
  await mgr.updateItem('dragoncraft-system-id', 'dragon-scale-component-id', {
    salvage: {
      enabled: true,
      ingredientQuantity: 1,
      resultGroups: [
        {
          id: 'rg-scales',
          name: 'Scale Shards',
          results: [{ componentId: 'scale-shard-component-id', quantity: 2 }]
        }
      ]
    }
  });
});

deleteItem(systemId, itemId)

Removes a component from the system. GM only.

When a component is deleted, any essence definitions that had their sourceItemUuid pointing to that item are automatically updated. Their sourceItemUuid is set to null.

Returns: Promise<boolean>


Essence Methods

getEssenceDefinitions(systemId)

Returns all essence definitions for a system.

Returns: object[]

Each returned object has the following shape:

Field Type Description
id string Unique identifier derived from the name
name string Display name
description string Flavour text (may be empty)
icon string FontAwesome class string. Always a non-empty string, and defaults to fas fa-mortar-pestle.
sourceItemUuid string\|null Authoritative field. The componentId of the component linked to this essence, or null.
const mgr = game.fabricate.getCraftingSystemManager();
const essences = mgr.getEssenceDefinitions('alchemy-system-id');
for (const ess of essences) {
  console.log(`${ess.name} (${ess.id}) — icon: ${ess.icon}`);
}

getEssenceDefinition(systemId, essenceId)

Returns a single essence definition by its ID.

Parameter Type Description
systemId string System ID
essenceId string Essence ID

Returns: object | null

const ess = mgr.getEssenceDefinition('alchemy-system-id', 'fire');
if (ess) {
  console.log(`Fire essence icon: ${ess.icon}`);
  console.log(`Source item: ${ess.sourceItemUuid ?? 'none'}`);
}

Internal Normalisation Helpers

These methods are called automatically by createSystem, updateSystem, createItem, addItemFromUuid, addItemsFromPack, and updateItem. You do not call them directly, but understanding them helps when inspecting or migrating stored data.

_normalizeCraftingCheck(check)

Normalises the craftingCheck object on a crafting system. Applies defaults for all fields including enabled, consumption, outcomes, and the per-mode simple, routed, and progressive sub-objects (each with its own rollFormula). enabled is the on/off toggle for the optional simple-mode check. A check becomes usable only when its resolution-mode sub-object carries an authored rollFormula.

_normalizeSalvage(salvage)

Normalises the salvage sub-object for a single component. Called by _normalizeComponent when features.salvage is true on the system.

Applies defaults: enabled: false, allowPlayerResultReorder: true, ingredientQuantity: 1, toolIds: [], resultGroups: []. The optional fields outcomeRouting, timeRequirement, and currencyRequirement are included only when present and non-null in the input.

enabled is normalised as salvage.enabled === true && resultGroups.length > 0, so it is clamped to false whenever the normalised resultGroups is empty. This enforces the Component invariant that an enabled salvage configuration has at least one result group. The normaliser is the single chokepoint every writer passes, so the clamp holds for API writes, imports, copy-mode, and migrations alike, not only for the GM editor. The clamp only ever turns enabled off, never on, so it seeds nothing and no migration accompanies it.

allowPlayerResultReorder is the GM-authored permission for player re-ordering of this component’s progressive salvage result stages (issue 651). It is normalised as salvage.allowPlayerResultReorder !== false, so an absent key reads as true and only an explicit false pins the authored order. The same default is applied when the component carries no salvage object at all. It is read in progressive salvage mode only, and it replaces the retired system-level salvageCraftingCheck.progressive.allowPlayerReorder.

_normalizeToolIds(toolIds)

Normalises salvage.toolIds (the library Tool ids required for the salvage operation). Coerces to trimmed, non-empty, deduped strings. Tolerant of non-array / nullish input (returns []).

_normalizeSalvageResult(result)

Normalises a single entry in a result group’s results array. Uses componentId as the identifier field. quantity must be a positive finite number. Invalid values fall back to 1.

_normalizeSalvageResultGroup(group)

Normalises a single entry in salvage.resultGroups. Assigns a random ID when id is absent. Falls back to "Result Group" when name is absent or empty. Normalises each entry in results via _normalizeSalvageResult. Returns null for invalid input. null entries are filtered out.

_normalizeTimeRequirement(time)

Normalises a time requirement object. Accepts any combination of minutes, hours, days, months, and years. Only keys whose values are positive finite numbers are included in the output. Zero, negative, or non-numeric values are dropped entirely.

_normalizeCurrencyRequirement(currency)

Normalises a currency requirement object. unit defaults to "gp" when absent or empty. amount must be a positive finite number. Invalid values produce 0.

_normalizeSalvageCraftingCheck(check)

Normalises the system-level salvageCraftingCheck object. See the field reference table in createSystem above for the full default and validation rules applied by this method.


This site uses Just the Docs, a documentation theme for Jekyll.