-
Notifications
You must be signed in to change notification settings - Fork 3
Modpack Guide KubeJS
Home › KubeJS Guide
Loader note: the KubeJS integration is currently NeoForge-only.
If your pack already lives in KubeJS, group rules can live right next to it. One event, one call per group:
RecipeViewerEvents.groupEntries(entryType, event => {
event.group(filter, groupId, description)
})Do you actually need KubeJS? The in-game editor builds the same groups — including full AND/NOT/namespace rules — and saves them as files you can ship in config/collapsiblegroups/groups/. Use KubeJS when you want rules versioned with your scripts, or generated programmatically. Use the editor when you just want groups.
-
'item'and'fluid'— always available. - Custom types like
'mekanism:chemical'(or a short alias like'chemical') — available only when the owning mod or an integration registered them in code viaCGApi. Mekanism chemicals and Productive Bees come pre-wired. - Heads-up: the in-game editor auto-discovers every JEI ingredient type, but KubeJS event targets are fixed before that discovery runs. If a type isn't pre-registered, scripts can't target it — that's a loader timing limit, not a bug. See Java API for registering types from a mod.
Three tiers, from most to least reliable:
1. Structured filters — use these whenever you can. They compile directly into the same rule model the editor uses, survive reloads, and stay readable:
-
'minecraft:barrel'— exact ID -
'#minecraft:logs'— tag -
'block:#minecraft:logs'— block tag (item groups only) -
'@create'— whole namespace -
['a', 'b', '#tag', '@mod']— an array is OR -
{ itemNamespace: 'mcwfurnitures', itemPathEndsWith: '_chair' }— object filters; multiple keys mean AND. Supported keys:itemId,itemTag,itemNamespace,blockTag,itemPathStartsWith,itemPathContains,itemPathEndsWith -
Ingredient.of(...).or(...)/.and(...)/.except(...)— composition..or()compiles to Any,.and()to All,.except()to All(base, Not(subtracted)). If any operand can't compile structurally, the whole expression drops to tier 2.
2. JS predicates — fallback, items and fluids. stack => stack.id.endsWith('_chair') works, but it scans what JEI shows at load time and freezes the result as a static snapshot. If visibility changes later, the group doesn't follow until groups reload. Fluid predicates additionally lower to plain fluid IDs, so component-level fluid distinctions are lost. Fine for quick experiments; for long-term rules prefer a structured form. Component checks (stack.hasComponent(...)) fall in this tier too — if you need a stable component rule, build it in the editor's Rules tab or the config file instead.
3. Unsupported. JS functions and regex-style patterns for custom types (they only snapshot for items/fluids). If you're reaching for these, the rule probably wants to be a structured filter or an editor-made group.
The bread and butter. A few copyable patterns:
// Plain OR — a handful of specific things
RecipeViewerEvents.groupEntries('item', event => {
event.group(
['minecraft:barrel', 'minecraft:crafting_table', 'minecraft:chest'],
'mypack:utility_blocks',
'Utility Blocks'
)
})// Mix IDs, tags, and namespaces freely in one OR
RecipeViewerEvents.groupEntries('item', event => {
event.group(
['#minecraft:logs', '@create', 'minecraft:barrel'],
'mypack:woodish_and_create',
'Woodish and Create'
)
})// AND — both sides must match (intersection, not merge!)
RecipeViewerEvents.groupEntries('item', event => {
event.group(
Ingredient.of('#minecraft:logs').and(Ingredient.of('@minecraft')),
'mypack:vanilla_logs',
'Vanilla Logs'
)
})// EXCEPT — everything in the tag, minus one
RecipeViewerEvents.groupEntries('item', event => {
event.group(
Ingredient.of('#minecraft:planks').except(Ingredient.of('minecraft:oak_planks')),
'mypack:planks_except_oak',
'Planks Except Oak'
)
})// Name-pattern matching without a JS predicate
RecipeViewerEvents.groupEntries('item', event => {
event.group(
{ itemNamespace: 'mcwfurnitures', itemPathEndsWith: '_chair' },
'mypack:macaw_chairs',
'Macaw Chairs'
)
})Three different matching strategies, one group, zero predicates:
RecipeViewerEvents.groupEntries('item', event => {
event.group(
[
{ blockTag: 'minecraft:logs', itemNamespace: 'minecraft' }, // vanilla log family, by block tag
{ itemNamespace: 'mcwroofs', itemPathStartsWith: 'gutter_' }, // prefix-named mod family
{ itemNamespace: 'mcwfurnitures', itemPathEndsWith: '_chair' } // suffix-named mod family
],
'mypack:wood_and_macaw_mix',
'Wood and Macaw Mix'
)
})Each branch stays structured and readable; the array gives you the OR. Need an exclusion on top ("…but never oak")? Wrap it: Ingredient.of(/* the OR above */).except(Ingredient.of('minecraft:oak_log')) — nesting stays fully structured as long as every operand compiles. When a rule gets deep enough that the script stops being readable, that's your cue to build it in the editor instead.
Keep fluid filters simple: IDs, #tag, @namespace, and OR arrays.
RecipeViewerEvents.groupEntries('fluid', event => {
event.group(['minecraft:water', 'minecraft:lava'], 'mypack:basic_fluids', 'Basic Fluids')
event.group(['#c:water', '@create'], 'mypack:water_and_create', 'Water and Create Fluids')
event.group('@mekanism', 'mypack:mekanism_fluids', 'Mekanism Fluids')
})Same shapes as fluids, on a registered type:
RecipeViewerEvents.groupEntries('mekanism:chemical', event => {
event.group('mekanism:hydrogen', 'mypack:hydrogen', 'Hydrogen')
event.group('#mekanism:gases', 'mypack:gases', 'Gases')
event.group(
['mekanism:oxygen', 'mekanism:hydrogen', '#mekanism:gases'],
'mypack:basic_gases_plus_tag',
'Basic Gases Plus Tag'
)
})- Namespace every
groupId(mypack:...) — collisions override silently. - One logical family per script file beats one giant file.
- KubeJS groups are source-read-only in the player editor: players can toggle enabled, and that's it — until they use Copy as Custom, which gives them a fully editable copy (the original gets disabled by default). If read-only is intentional, say so in your pack notes.
- Your
groupIdbecomes a stable internal ID that expansion/enabled state persists against: items get__kjs_<groupId>, fluids__kjs_fluid_<groupId>, custom types__kjs_<typeId>_<groupId>(with:and/becoming_). Rename agroupIdand players' expand/enabled state for it resets. - Troubleshooting order: is the group enabled? → does JEI currently show ≥2 members (and is search ungrouping small results)? → did a higher-priority group claim them first? → typo in the filter? → is this a predicate that snapshotted stale visibility? → for custom types: was the type pre-registered by a mod?
Players
Modpack Developers
Mod Developers
Localization