Skip to content

Action Files

Every action is one HOCON file in config/showdown_actions/actions/. The filename without the .conf extension is the action’s id by default; an optional id = "..." field at the top level overrides it.

A complete action file is up to four blocks:

# Top-level fields - id, displayName, enabled, priority, target, chance, oncePerBattle, permission, mutexGroup
id = "..."
priority = 0
target = "SELF"
# What gates the action - sugar fields + optional whenMolang
match {
...
}
# What happens when the gate passes - sugar effects
apply {
...
}
# Optional tier-3 escape hatch - raw Showdown JS hooks
showdown {
...
}

Of those, only id and one of apply {} / showdown {} are strictly required to be useful. An action with no match clause matches every Pokémon in every battle (which is rare but legal). An action with an empty apply and no showdown is a no-op — useful only as a debug placeholder for testing predicates.


ShowdownActions is built around three escalating tiers of authoring power. The tier you’re at is determined by which fields you populate, not by any explicit “tier” switch. You can mix tiers freely in a single action — e.g. tier-1 sugar effects gated by a tier-2 Molang predicate.

Fill in match and apply with predefined sugar fields. The gate becomes a set of property checks plus (where needed) a Molang predicate, and the effect becomes an auto-applied Showdown condition with an onStart hook.

id = "lycanroc_dusk"
displayName = "Lycanroc - Dusk Form"
target = "SELF"
match {
species = "lycanroc"
sugar {
time = "dusk"
}
}
apply {
aspect = "dusk"
boosts { spe = 1, atk = 1 }
message = "{name} resonates with the dusk light!"
}

This covers most cases. See Match Clause and Apply Clause for every sugar field.

When the sugar gates don’t compose into the condition you want, set whenMolang to any expression Cobblemon’s Molang runtime can evaluate. The expression is ANDed with whatever sugar fields you also populated — you don’t have to choose between them.

id = "rainy_swift_swim"
match {
ability = "swiftswim"
whenMolang = "q.world.is_raining && q.pokemon.level >= 30"
}
apply {
boosts { spe = 2 }
}

The runtime binds q.pokemon, q.player, q.world, and q.battle for you. Read the Molang Predicates page for the full surface, including the helpers ShowdownActions registers under q.world.* (is_time, dimension_string, etc.).

When sugar can’t reach the effect (multi-handler conditions, custom turn logic, anything that depends on Showdown’s internal API), drop a showdown { } block. The hooks register straight into Dex.data.Conditions[…] — the same place Showdown stores its own built-in conditions.

id = "custom_intimidate"
match { species = "gyarados" }
showdown {
conditionId = "showdownactionsgyaradosintimidate"
conditionName = "Sigil Intimidate"
scope = "volatile"
hooks {
onStart = """function(pokemon) {
var foe = pokemon.side.foe.active[0];
if (foe && !foe.fainted) {
this.boost({atk: -2}, foe, pokemon);
this.add('-message', pokemon.name + ' glares menacingly at ' + foe.name + '!');
}
}"""
}
}

Any Showdown handler is supported — onStart, onModifyAtk, onAnyDamage, onResidual, onTryHit, onAfterMoveSecondary, the lot. The hook name goes straight in as a map key under hooks { } and is passed verbatim to Showdown. See Raw Showdown Hooks for the catalogue and Showdown Research for guidance on learning the engine well enough to write them.

When a showdown {} block is present, ShowdownActions does not auto-generate a species/form guard around your hooks. You have full control of what fires, and you’re responsible for adding identity checks if your hook needs them.


These fields apply to the action as a whole, regardless of tier.

FieldTypeDefaultPurpose
idStringfilename without .confStable identifier. Used by /showdownactions info <id> and command suggestions.
displayNameString""Free-form label shown in admin GUIs/logs and as the condition’s name in Showdown. No behavioral effect.
enabledBooleantrueWhen false, the file is loaded but skipped at every match attempt. Treat false as “comment out without deleting”.
priorityInt0Higher priority is evaluated first when several actions match the same (actor, pokemon). Mostly relevant inside a mutexGroup.
targetenumSELFWhich side+slot the resulting Showdown effect is registered against. See Targeting.
chanceDouble1.0Probability gate in [0, 1]. Rolled once per battle, before the effect is registered. 1.0 always fires.
oncePerBattleBooleanfalseFire at most once per battle, regardless of how many times the matched Pokémon switches in.
permissionString?nullOptional permission node. When set, the matching player must satisfy it (via fabric-permissions-api) for the action to fire.
mutexGroupString?nullMutual-exclusion key. Within one battle and trainer, only the highest-priority matching action whose mutexGroup equals this string fires; the rest are dropped. Use it to swap form variants without writing negative predicates.

The canonical use case is form rotation. Three Lycanroc variants — Midday, Dusk, Midnight — with mutexGroup = "lycanroc_form" and different sugar.time predicates. Whichever matches the current time wins the group; the others are silently dropped, even though they’re also valid actions.

# Three files, all with mutexGroup = "lycanroc_form"
# - lycanroc_midday.conf: sugar.time = "day", priority = 5
# - lycanroc_dusk.conf: sugar.time = "dusk", priority = 5
# - lycanroc_midnight.conf: sugar.time = "night", priority = 5

Mutex resolution happens after chance and permission gates pass. Dropped losers are logged at trace level when debug = true.


ShowdownActions loads action files when the server finishes starting. The first time the server starts with the mod installed, the bundled 00_bible.conf, example_*.conf, recipe_*.conf, and a few working samples are copied into actions/ (only if actions/ is empty). On subsequent starts, your edits are preserved.

/showdownactions reload re-runs the same pipeline:

  1. Re-parses config.conf. The debug flag updates immediately.
  2. Walks actions/ and re-parses every .conf.
  3. Swaps in the new ruleset all at once — mid-flight battle-start hooks never see a half-loaded state.
  4. Re-registers Showdown conditions for every action whose apply (or showdown) emits a Showdown-side effect. Each newly-registered condition is propagated to every cached ModdedDex instance so format-specific battles can resolve it.

Battles that are already in progress keep the conditions they were registered with at the start of that battle. New battles pick up whatever’s loaded now.

If a single .conf fails to parse, the rest of the ruleset is preserved. The failing file is logged with its parse error and the previous version of that action (if any) stays live until the file parses again.


What’s “Re-Evaluated” and What Isn’t

Section titled “What’s “Re-Evaluated” and What Isn’t”

Match-clause fields fall into two buckets: gates that re-check on every switch-in, and gates that are static for the whole battle.

Re-checked on every switch-inStatic for the whole battle
species, form, type, ability, heldItem, teraType, minLevel, maxLevelaspect, nature, gender, minFriendship, isLegendary, sugar.*, whenMolang

The re-checked set is enforced by an auto-applied guard inside the switch-in hook. This matters because in this Cobblemon environment Showdown’s onStart re-fires on every switch-in — without the guard, swapping Pikachu out and Bulbasaur in would re-apply a Pikachu-targeted effect to Bulbasaur. The static set is evaluated once at battle start and not re-checked, because the values can’t change mid-battle.

If you write a custom showdown {} block (tier 3), no guard is generated — you control the entire effect, and you’re responsible for any identity checks.


  • Match Clause — every sugar gate field, what it filters on, and when it re-evaluates.
  • Apply Clause — every sugar effect field and the Showdown JS it compiles to.
  • Targeting — the five targeting modes and when to use each.
  • Molang Predicates — the whenMolang surface, bindings, and helpers.
  • Recipes — working examples to copy from.