Skip to content

Modpack Guide KubeJS

StarskyXIII edited this page Jul 17, 2026 · 4 revisions

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.

Entry Types

  • '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 via CGApi. 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.

What Filters Can I Pass?

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.

Items

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'
  )
})

The mixed example worth stealing

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.

Fluids

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')
})

Custom Types

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'
  )
})

Good Habits

  • 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 groupId becomes 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 a groupId and 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?

Home


Players


Modpack Developers


Mod Developers


Localization

Clone this wiki locally