Data Models
These model classes are exported via game.fabricate.api:
const { Recipe, Ingredient, IngredientGroup, IngredientSet, Result } = game.fabricate.api;
IngredientSet and Result are documented below as recipe data shapes. They are normalised through recipe and manager APIs, but they are not exported as public constructors.
Recipe
new Recipe({
id, // string -- auto-generated if omitted
name, // string
description, // string
category, // string
craftingSystemId, // string -- links to a crafting system
enabled, // boolean (default true)
locked, // boolean (default false)
allowPlayerResultReorder, // boolean (default true) -- progressive mode only
linkedRecipeItemUuid, // string | null
craftingModifier: {
// object | null (default null) -- the recipe author's check-modifier pick
modifierIds, // string[]
},
visibility: {
restricted, // boolean (default false)
allowedUserIds, // string[]
},
ingredientSets, // Ingredient-set data[] (single-step)
steps, // object[] (multi-step)
resultGroups, // object[]
toolIds, // string[] (library Tool ids required for crafting)
transferEffects, // boolean (default false)
resultSelection: {
// object (legacy, retired — round-trip only)
provider, // "ingredientSet" | "check"
},
metadata, // object (created, modified, author, version)
});
allowPlayerResultReorderis the GM-authored permission for player re-ordering of this recipe’s progressive result stages (issue 651). It defaults totrue, and an absent key reads astrue, which is why the 1.17.0 migration does not seed it. Only an explicitfalsepins the authored stage order. It is read in progressive mode only, and it replaces the retired system-levelcraftingCheck.progressive.allowPlayerReorder. The salvage equivalent iscomponent.salvage.allowPlayerResultReorder.
craftingModifieris the recipe author’s pick of the system’s check modifiers (issue 770, reshaped by issue 1055). It authors one axis — which modifiers apply — and never how they combine: the combination rule is the system’scraftingCheck.defaultModifierPolicyalone. See CraftingSystemManager.null(the default) picks nothing and inherits the system’sdefaultModifierIds.modifierIdsis authored or absent, not merely non-empty or empty. An authored empty array ([]) is a real pick meaning “no eligible modifiers” (nothing is added to the check roll), distinct from an absentmodifierIds, which inherits. A pick is honoured only under the system’s"bySubject"rule — rendered “By recipe” on this activity — and is then truncated tocraftingCheck.maxModifierPicksand clamped to each entry’s ownmin/max; under"addAll","highest", and"playerPicks"it stays on disk unread. Its siblings on the other two activities areComponent.salvage.checkModifierIdsandGatheringTask.checkModifierIds, which obey the identical authoredness rule. The pre-1095 spelling"byRecipe"is still accepted on read and is never written back. Neither switching the rule away nor lowering the cap deletes anything, and restoring either applies the picks again with nothing to re-author. A legacypolicykey from before issue 1055 is dropped by the normaliser, never round-trips throughtoJSON(), and is never consulted wherever it survives unnormalised.
The per-recipe
resultSelection.provideris retired (issue 554). No live resolution mode reads it: alchemy routing moved to the system-level alchemy check mode (system.alchemy.checkMode), and the two routed crafting modes (routedByIngredientsandroutedByCheck) derive their routing basis from the system mode. The field is still normalised so a legacy recipe round-trips until migration strips it, but authoring no longer sets it.routedByIngredientsroutes by eachIngredientSet.resultGroupId, androutedByCheckroutes by the system’s routed crafting-check outcome. The legacyoutcomeRoutingfield and the legacymapped/tieredand singleroutedmodes are normalised on load.mappedbecomesroutedByIngredientsandtieredbecomesroutedByCheck.
Key methods:
| Method | Returns | Description |
|---|---|---|
validate() | {valid, errors} | Validates recipe structure |
getResultDescription() | string | Human-readable result summary |
isSimpleRecipe() | boolean | True if no tags/essences/tools/steps |
getExecutionSteps() | object[] | Steps array (converts implicit step if single-step) |
toJSON() | object | Serialise to JSON |
Recipe.fromJSON(data) | Recipe | Deserialise from JSON |
Recipe.createSimple(name, ingredients, result) | Recipe | Helper for simple recipes |
IngredientSet Data Shape
{
(id, // string
name, // string
ingredientGroups, // IngredientGroup[] -- all must be satisfied (AND)
essences, // { [essenceId]: quantity }
toolIds, // string[] (library Tool ids required for this set)
resultGroupId); // string | null (routedByIngredients routing target)
}
Key methods:
| Method | Returns | Description |
|---|---|---|
Recipe.validate() | {valid, errors} | Validates nested ingredient-set data as part of the recipe |
RecipeManager.evaluateCraftability(recipe, actors) | object | Evaluates whether an actor inventory can satisfy ingredient-set data |
IngredientGroup
IngredientGroup.fromJSON({
id, // string
name, // string
options, // Ingredient[] -- any one satisfies the group (OR)
});
Ingredient
new Ingredient({
quantity, // number (default 1)
match: {
type, // "component" | "tags" | "currency" | "essence"
componentId, // string (for component type)
tags, // string[] (for tags type)
tagMatch, // "any" | "all" (for tags type)
unit, // string (currency unit id, for currency type)
essenceId, // string (for essence type)
amount, // number (currency cost or essence amount, for currency/essence types)
},
extractEffects, // boolean (default false)
effectFilter, // string | null (regex for filtering effects)
});
The
match.typevalue"component"replaces the previous"systemItem". Thematch.componentIdfield replaces the previousmatch.systemItemId. Use the new names for all new data.
Key methods:
| Method | Returns | Description |
|---|---|---|
matches(item) | boolean | Check if a Foundry item satisfies this ingredient |
getDescription() | string | Human-readable description |
validate() | {valid, errors} | Validates structure |
Tool
The standalone
Catalystmodel was removed in0.6.0. Tools are not constructed viagame.fabricate.api. They are authored in the per-system Tools library through the Crafting System Manager and referenced by id (toolIds). See Tools for the full concept.
A Tool entry stored under system.tools (the craftingSystems setting) has this shape:
{
(id, // string (library id, referenced by toolIds)
componentId, // string | null (optional managed-component link; null for an item-sourced tool)
name, // string | null (display snapshot captured at registration/migration)
img, // string | null (display snapshot image)
registeredItemUuid, // string | null (the tool's own registered source item uuid)
originItemUuid, // string | null (the tool's own canonical/compendium source uuid)
aliasItemUuids, // string[] (additional source references for matching)
label, // string (optional user-authored display label, distinct from the snapshot)
requirement, // null | { formula } (a Foundry roll expression; required when set)
prerequisites, // { enabled, ids, gateMode: 'bonus' | 'usability' }
bonus, // { enabled, expression }
breakage, // { mode: 'limitedUses', maxUses } |
// { mode: 'breakageChance', breakageChance } |
// { mode: 'diceExpression', formula, threshold }
checkBreakable, // boolean (default true; participates in check-driven breakage)
onBreak, // { mode: 'destroy' } | { mode: 'flagBroken' } |
// { mode: 'replaceWith', replacementTarget }
repairRequirements); // IngredientGroup[] (for flagBroken; may be empty)
}
A Tool is first-class as of issue 561: it carries its own source references (registeredItemUuid / originItemUuid / aliasItemUuids, renamed in issue 560) and a name / img display snapshot, so it can be registered directly from an Item without importing that Item as a component. componentId is optional. It is null for an item-sourced tool and populated only for a tool that is also a managed component (a whetstone) or one migrated from a legacy component-linked tool. A valid Tool carries either a componentId or its own source references.
prerequisites references the crafting system’s shared character prerequisites. Its gate mode either controls whether the character can use the Tool or suppresses its enabled bonus. bonus is an optional check expression. checkBreakable: false excludes the Tool from check-driven breakage only. It does not replace the retained tool-specific breakage mechanic. For replaceWith, replacementTarget is exactly one managed Component or direct Item UUID. The current Tool Studio authors managed Component targets only. Direct Item targets remain readable and executable for backward compatibility with existing data. repairRequirements holds the optional ingredient groups used to repair a Tool marked as broken.
Per-item usage for limitedUses tools is tracked under Item.flags.fabricate.toolUsage = { timesUsed }. The counter changes only while the system uses Tool-specific breakage. Under check-driven authority the retained Tool-specific configuration and its counters are inactive and unchanged. The flagBroken on-break action sets Item.flags.fabricate.toolBroken = true. A tool’s durable identity is stamped on its source Item as Item.flags.fabricate.roles[systemId].toolId, a sibling of the component role flag, so the same Item can be both a component and a tool.
Result Data Shape
{
(id, // string
componentId, // string (managed component reference)
itemUuid, // string (direct Foundry item reference)
quantity, // number (default 1) -- the AUTHORED, fixed amount
quantityFormula, // string | null -- a non-empty value means the amount is ROLLED instead
propertyMacroUuid); // string | null
}
The field was previously named
systemItemId. UsecomponentIdfor all new data.
Result data is validated as part of recipe validation and consumed by the crafting engine when a result group is awarded.
A non-empty
quantityFormulamakes the amount ROLLED, andquantitystays the authored amount it falls back to, rather than the number awarded. Setting a formula never overwritesquantity, so clearing the formula returns the result to the number the GM typed, with nothing to re-enter. The formula is a roll expression, resolved once per result per award against the crafting character, and it can reference the character’s own roll data.Result.validate({ Roll })acceptsRollas an injected dependency and rejects a formula with no character reference whose maximum possible roll can never exceed zero. With noRollsupplied, validation reports nothing aboutquantityFormula. An empty, whitespace-only, or absentquantityFormulaleaves the amount fixed atquantity, which is the state of every result created before this field existed.
Related methods:
| Method | Returns | Description |
|---|---|---|
Recipe.validate() | {valid, errors} | Validates nested result data as part of the recipe |
Recipe.getResultDescription() | string | Human-readable result summary |