Skip to content

Recipe Definitions ​

Crafting recipes live under plugins/EmakiStation/recipes/, one recipe per file. The file name does not identify the recipe: the recipe id comes from the id field inside the file

Matching is unordered set matching: only material kinds and totals matter, never where you place them, and there is no shaped concept

Full Example ​

yaml
id: "iron_ingot_block"
display_name: "<white>Iron Block Pressing</white>"

station_ids:
  - "blacksmith"

tags:
  - "blacksmith"

permission: ""
visible: true
duration_seconds: 30

cost:
  currency:
    type: "vault"
    amount: 0

materials:
  - material_id: "iron_ingot"
    requirement_id: "iron_ingot_input"
    count_key: "iron_ingot"
    item_sources:
      - "minecraft-iron_ingot"
    amount: 9
    consume: true

result:
  outputs:
    - item_source: "minecraft-iron_block"
      amount: 1
  actions: []

condition:
  invalid_as_failure: true

display_condition:
  invalid_as_failure: false

Top-Level Fields ​

FieldTypeRequiredDefaultAccepted rangeDescription
idstringYes——Recipe id, unique across the whole recipe directory. A missing id refuses the file (station.recipe_missing_id)
display_namestringNothe idMiniMessage textDisplay name
station_idslist<string>No[]station idsWhich stations this recipe belongs to; empty means open to every station. Values are lower-cased before matching
tagslist<string>No[]stringsStill usable for other purposes such as API queries, but no longer drives station ownership
permissionstringNo""a permission nodePermission required to use this recipe; empty means unrestricted
visiblebooleanNotruetrue, falseWhether the recipe appears in the catalog. false hides it completely, usually paired with API submission
duration_secondslongNo0≥0; negatives clamp to 0Duration of one craft, in seconds. 0 or omitted completes immediately without entering the queue
costsectionNofreesee belowExtra currency charged per craft, multiplied by the batch
materialslistYes—non-empty listMaterial requirement list. An empty list refuses the recipe (station.recipe_no_materials)
resultsectionYes—see belowOutput section. Missing or output-less refuses the recipe (station.recipe_no_outputs)
actionssectionNoall emptysee belowPer-stage action lines; parsed but not executed today
conditionsectionNoemptysee belowCondition gate checked before submission
display_conditionsectionNoemptysee belowUnlock condition deciding whether the catalog entry is greyed out

The station-side recipes section (include_tags / include_ids / exclude_ids) is deprecated; declare ownership on the recipe side through station_ids instead

Without the permission node, the recipe does not appear in the catalog at all -- it does not even show greyed out

yaml
# Example: a recipe open to every station, permission-free and instant
id: "quick_charcoal"
display_name: "<gray>Quick Charcoal</gray>"
duration_seconds: 0
materials:
  - item_sources:
      - "minecraft-oak_log"
    amount: 1
result:
  outputs:
    - item_source: "minecraft-charcoal"
      amount: 1

cost — Currency Cost ​

FieldTypeRequiredDefaultAccepted rangeDescription
costsectionNofree—A missing currency sub-section is silently treated as free, with no log line
cost.currency.typestringNo""vault, excellent (excellenteconomy is equivalent)Economy provider. Any other non-blank value logs station.recipe_bad_currency and is treated as free. When both it and amount are empty the recipe is free
cost.currency.amountlongNo0>0 to chargeAmount. Zero or below logs station.recipe_bad_cost_amount and is treated as free

WARNING

type and amount must live inside the cost.currency sub-section, not directly under cost. When the currency sub-section is missing the plugin reports nothing, logs nothing and simply treats the recipe as free, so watch out -- such a configuration fails silently

The plugin checks your balance before spending materials and charges after they are spent; if the charge fails, the materials are refunded in full. Cancelling a queue entry refunds at the station's cancel_refund_rate

materials — Material Requirements ​

materials is a list, and each element is one requirement

FieldTypeRequiredDefaultAccepted rangeDescription
material_idstringNo"<recipe_id>.material.<index>"—Material selection identity. When omitted it is derived from the index and a station.recipe_derived_material_identity warning is logged; new configuration should declare it explicitly. A duplicate with another material_id/requirement_id blocks loading (station.recipe_duplicate_material_identity)
requirement_idstringNomaterial_id—Requirement-record and queue-credential identity
count_keystringNomaterial_id—Quantity aggregation/consumption-record identity
item_sourceslist<string>No[]item source tokens (hyphen shorthand)An "any of these" set whose amount is counted across the whole set. Omitting it leaves the item source unrestricted
item_sourcestringNo—a single item source tokenLegacy form. Written together with item_sources it is refused (station.recipe_source_field_conflict); written alone it is read but logs station.recipe_legacy_item_source
matchersection/objectNo—general item matcherOnly non-item-source conditions (component, PDC, lore). ANDed with the sibling item_sources. See Item Matcher
amountlongNo1>0Total amount required. Zero or below logs station.recipe_bad_amount and skips that requirement
consumebooleanNotruetrue, falsefalse validates possession without spending, for requirements like "hold a tool"

Leave both item_sources and matcher out and the requirement is skipped. For a requirement of 64, 30 coal plus 34 charcoal also satisfies it

The number of material kinds per recipe is not limited by GUI slots; passing limits.warn_material_types only logs a warning. The warehouse channel deducts long amounts directly, while the inventory channel is bounded by the vanilla 64-per-slot limit

Material allocation uses a deterministic maximum-flow solver: when you submit, the plugin puts every inventory stack and every warehouse item source into one graph and searches for a complete assignment across all requirements, instead of relying on declaration order. Narrower matcher requirements join the graph first, but the outcome is still judged globally; a failure means the current candidates genuinely cannot satisfy all requirements at once

General item matcher ​

Two sibling fields decide a materials[] entry, item_sources and matcher, and both must pass: item_sources carries the allowed item sources, matcher carries only non-item-source conditions such as component, PDC, or lore. Leave both out and the material never matches. Full syntax is in Item Matcher

yaml
materials:
  # Iron ingots that have not been renamed
  - item_sources:
      - minecraft-iron_ingot
    matcher:
      type: component
      component: custom_name
      operator: absent
    amount: 9
    consume: true

WARNING

Never write item source conditions inside matcher. type: item_source / item_sources / source / sources are rejected with a warning, and the matcher never matches

A material with a matcher is supplied from the inventory only

The one material that declares a matcher can be supplied from the inventory only; storage lookup skips it. Storage counts stock by item source and cannot inspect a real item's components, so it cannot prove a component condition

At load time a single storage_unreachable_material WARN is logged for each such material (only when the recipe belongs to at least one station with channels.storage enabled and storage.enabled: true)

If a material must draw from storage, declare only item_sources for it, with no matcher. Other materials in the same recipe that only declare item_sources can still use the warehouse channel normally

result — Outputs ​

FieldTypeRequiredDefaultAccepted rangeDescription
result.outputslistYes—non-empty listOutput list
result.outputs[].item_sourcestringYes—a single item source tokenOutput source, singular; a list or an unparsable token logs station.recipe_bad_source and refuses the whole recipe
result.outputs[].item_sourceslistNo—exactly 1 elementLegacy form; still read but logs station.recipe_legacy_item_source, and refused when combined with item_source
result.outputs[].amountlongNo1>0Output amount per craft; zero or below logs station.recipe_bad_amount and skips that entry
result.actionslist<string>No[]action pipeline linesPost-settlement action lines; parsed but not executed today

An output node does not allow a matcher; writing one logs station.recipe_bad_source and refuses the whole recipe

actions — Per-Stage Action Lines ​

FieldTypeRequiredDefaultAccepted rangeDescription
actions.prelist<string>No[]action pipeline linesPre-submit action lines; parsed but not executed today
actions.successlist<string>No[]action pipeline linesSuccess action lines; parsed but not executed today
actions.failurelist<string>No[]action pipeline linesFailure action lines; parsed but not executed today

These four lists (including result.actions) live on the recipe object and are readable through the API, but the current runtime has no execution entry point, so writing them has no effect

The Two Condition Gates ​

SectionTypeRequiredDefaultAccepted rangeDescription
conditionsectionNoempty (always passes)see ConditionsDecides whether it can be crafted. Evaluated during submission validation; on failure it returns station.recipe_condition_failed
display_conditionsectionNoempty (always unlocked)same as aboveDecides whether it is unlocked. Evaluated on every catalog repaint; on failure the entry shows as a greyed-out placeholder and clicking does nothing
SectionDecidesBehaviour when unsatisfied
conditionWhether it can be craftedThe recipe still shows; clicking is refused
display_conditionWhether it is unlockedThe recipe shows as a greyed-out placeholder and clicking does nothing

invalid_as_failure behaves as in other modules, and the source default for both condition and display_condition is true (the shipped example writes display_condition explicitly as false, so a broken condition leans towards "unlocked" rather than locking the recipe away). Note that both sections only evaluate the condition group itself: on_pass.actions / on_fail.actions / on_fail.message are not executed today

A greyed-out placeholder instead of hiding tells you there is more content ahead. To hide something completely, use visible: false or permission

display_condition is evaluated on every catalog repaint, so heavy conditions slow the window down