Skip to content

Action System ​

To write an action pipeline, describe line by line in YAML what should happen after a business event, then look up which stages the pipeline can use and what parameters each stage takes. The action system is one of the most frequently used CoreLib features, and shows up across Forge, Strengthen, Gem, Cooking, Skills, Item, Attribute, and Level

A pipeline is one line of text ​

Field names differ between modules — actions.success, actions, deny_actions, result.success.actions — but the model is the same: every line in the list is an independent pipeline, and the lines run in order

A pipeline chains stages with |, and data flows left to right:

text
self | chance 25% | after 20t | send_message text="<green>Triggered</green>"

Read that as: take the caster as the target, pass a 25% roll, wait 20 ticks, then send the message. What travels between stages is a target flow — a list of targets. That split is the point of the model: picking targets and acting on them are different stages. The old killentity radius=5 limit=3 type=zombie bundled the search into the effect; now the search is nearby and the effect is kill_entity

yaml
actions:
  success:
    - 'self | send_message text="<green>Forge success!</green>"'
    - 'self | play_sound sound=minecraft:entity.player.levelup volume=1 pitch=1'
    - 'run_command_as_console command="say %player_name% completed a forge"'

NOTE

Stage arguments are placeholder-resolved before execution. CoreLib supplies %player%, %player_name%, %player_uuid%, %player_world%, %player_x%, %player_y%, %player_z%, resolves whatever the calling module wrote into the context, and resolves PlaceholderAPI placeholders. Variables the pipeline sets itself read back as %var.<name>%. For module-specific variables, see that module's page

Third-party execution and structured results ​

Third-party plugins can compile and execute a complete pipeline with EmakiCoreLibApi.executeActionLineAsync(plugin, line, CoreActionExecutionContext). It is callable from any thread. The future may complete on the calling thread (for example, an immediate validation result) or in the final stage's execution domain, so schedule through EmakiCoreLibApi.scheduling() before a continuation touches Bukkit. Compilation and business failures complete normally as structured results; do not rely on exceptional completion alone

CoreActionExecutionResult contains status, reasonKey/reasonArguments, compile diagnostics, ordered stages summaries, and keptTargets. Statuses are SUCCESS, SKIPPED, PARTIAL, COMPILE_FAILED, EXECUTION_FAILED, INVALID_REQUEST, and UNAVAILABLE, allowing callers to distinguish no work, partial completion, invalid configuration, and runtime failure

The facade also exposes read-only registry queries: actionStages() / actionStage(id) and actionTriggers() / actionTrigger(id). For a custom stage, CoreActionExecutionTarget.contextEntity() selects the entity-context execution domain; planning contexts use context.target() or context.caster() (not subject()). Revoke the returned CoreStageRegistration with registration.close()

The compatibility callback onStageRegistryRebuilt(owner, callback) replaces the previous callback for the same owner. Use addStageRegistryRebuildListener(owner, callback) when one plugin needs multiple independent callbacks; each is appended independently and returns a closeable handle

Minimal third-party execution example:

java
CoreActionExecutionContext context = CoreActionExecutionContext.builder()
    .caster(player)
    .build();
EmakiCoreLibApi.executeActionLineAsync(this,
    "self | send_message text='<green>Hello</green>'", context)
    .thenAccept(result -> getLogger().info(result.status().name()));

Syntax ​

The authoritative grammar:

text
line     := node ('|' node)*
node     := branch | weighted | stage
branch   := 'if' condition '[' line ']' ('else' '[' line ']')?
weighted := 'weight' (weight '[' line ']') +
stage    := name arg*
arg      := key '=' value | bare_value

Key points:

  • A pipeline is always one line of text. There is no structured YAML form and no second parser
  • Stage name and arguments are separated by spaces, not parentheses. Write send_message text="...", not send_message(text="...")
  • Arguments come in two forms: key=value, or a bare positional value. Bare values map onto positional parameters in declaration order, so chance 25% is the same as chance chance=25%
  • Stage names are lower-cased, so Send_Message and send_message are equivalent. Argument names are lower-cased too
  • Blank lines and lines starting with # are ignored
  • Single and double quotes both work. Inside quotes, \n, \t, \\, \", and \' escape. Quoted |, [, and ] are ordinary characters, so the bar in text="a | b" is not a separator
  • Outside quotes, |, [, and ] split tokens even without surrounding spaces: a|b and a | b are identical
  • || is the escaped bar, which is also logical OR in conditions. It is never treated as a separator

Three roles ​

Every stage has a fixed role, and that role decides where it may appear in a pipeline:

RolePurposeExamples
sourceProduces the target flow. Usually the first stageself, looking_at, nearby radius=5
gateFilters, sorts, controls timing, or writes context valueschance 25%, where ..., after 20t
actionProduces the actual effect, applied to every target in flowsend_message, damage, give_money

The role belongs to the stage, not to the position, which is why /emakicorelib action list groups by role instead of printing one flat list: a given id is only legal in one kind of slot

Omitting the source defaults to self, so watch out for this default: to act on the targets your caller passed in, you must write inherited explicitly, or the pipeline silently acts on the caster

Branches ​

Conditions are no longer a prefix in front of a stage. They are a node in the pipeline:

text
if %player_world%=="world" [ self | send_message text="In the overworld" ] else [ self | send_message text="Somewhere else" ]

The else branch is optional. A branch body is a complete pipeline in its own right, with its own source and as many stages as you like. Conditions support &&, ||, !, parentheses, and the comparisons <, <=, ==, !=, >=, >. Quote the right-hand side of string comparisons so they are not mistaken for arithmetic. Remember that logical OR is || inside a branch condition

A branch body can also hold a single gate, for example stop to end early:

text
looking_at | if %var.dead% [ stop ] else [ damage amount=5 ]

Weighted branches ​

Where if picks one of two by condition, weight picks one of many by weight: each weight is followed by its own branch body, and at runtime exactly one body is chosen in proportion to its share of the total

text
self | weight 70 [ send_message text="<gray>Common</gray>" ] 25 [ send_message text="<blue>Rare</blue>" ] 5 [ broadcast_message text="<gold>Epic!</gold>" ]

Read this as: roll once at relative odds 70/25/5, then run only the body that was drawn. Weights need not total 100 — writing 7 2.5 0.5 gives identical probabilities, because a weight is a relative share. See Weights

Each weight sits directly in front of its own body rather than in one separate list. That is deliberate: had the weights been collected as [70, 25, 5] and matched positionally against the bodies that follow, inserting, deleting, or reordering a branch would silently misalign them, and that misalignment is invisible at load time — it only shows up as "the odds feel wrong"

Key points:

  • A branch body is a complete pipeline, exactly like an if body, with its own source and as many stages as you like
  • weight selects exactly one body. If you want each outcome judged independently so several can fire at once, that is multiple chance lines, not weight
  • A weight of 0 disables that branch; it can never be drawn. All weights being 0 is a load-time error, because the stage could then never select anything
  • Weights may be expressions or placeholders, for example weight %var.luck% [ ... ] 30 [ ... ]. Placeholders are rendered before execution, so the odds can follow player state
  • Weights must be non-negative. A negative weight is rejected at load time (literals) or reported at runtime (rendered placeholders) rather than ignored — otherwise one stray minus sign would make a branch vanish without a word
  • Weights are unnamed values, never name=value. weight chance=70 [ ... ] is an error
  • weight and if share the single nesting limit action.pipeline.max_branch_depth (default 16), and may be nested inside each other

weight and chance answer different questions. chance 25% is a single gate: miss the roll and the whole pipeline ends. weight is a multi-way choice that always selects something (unless every weight is 0). Use chance for "fires 25% of the time, otherwise nothing happens"; use weight for "always happens, but which outcome is decided by odds"

text
self | weight 1 [ nearby radius=5 | damage amount=8 ] 1 [ send_message text="<gray>Missed this time</gray>" ]
self | if %var.is_boss% [ weight 80 [ run drop_common ] 20 [ run drop_rare ] ] else [ run drop_trash ]

Named sequences and run ​

You can define a group of pipelines you reuse as a named sequence and call it with run:

yaml
action:
  templates:
    reward_common:
      - 'self | send_message text="<green>Reward claimed</green>"'
      - 'self | play_sound sound=minecraft:entity.experience_orb.pickup volume=0.8 pitch=1.2'
    level_up_effect:
      - 'self | send_title title="<gold>Level up!</gold>" subtitle="<gray>Now level %var.level%</gray>"'
      - 'self | play_sound sound=minecraft:entity.player.levelup volume=1 pitch=1'
      - 'self | spawn_particle particle=totem_of_undying count=30 offset_x=0.5 offset_y=1 offset_z=0.5'

Arguments go straight after run, and the sequence body reads them as %var.<name>%:

text
run reward_common
run level_up_effect level=10
run reward_with_amount amount=100 reason=daily_login

Sequences can live under action.templates in the CoreLib main config or in a business module's own data directory. run only cares about the name, not where it was defined. Names are case-insensitive

Sequences are compiled and cycle-checked at load time, so you see errors for mutual recursion, calls to names that do not exist, and missing required arguments when the config loads, not at runtime

Named aliases (aliases) ​

Give a frequently used pipeline fragment an alias and drop it into any pipeline line:

yaml
action:
  aliases:
    buff_essentials: 'give_potion_effect effect=speed duration=30s amplifier=0 | give_potion_effect effect=jump_boost duration=30s amplifier=0'
    punish_msg: 'send_message text="<red>Operation refused</red>"'
text
self | buff_essentials | damage amount=1
damage amount=3 on_error [ punish_msg ]

Rules:

  • Aliases expand into their original tokens at compile time; an alias may reference another alias (up to 8 levels; a cycle reports the alias_cycle diagnostic);
  • The reserved words if / else / run / weight / on_error / union / parse_if are never shadowed by an alias;
  • An alias whose name matches a registered stage id is skipped with a startup warning (the original stage is never silently swallowed);
  • Expanded column numbers shift to the alias position, so diagnostics still point at what you actually typed

Source union (union) ​

A pipeline may only contain one source stage; union [ ... ] merges several sources' results into one deduplicated target flow:

text
union [ all_players | nearby type=zombie radius=16 ] | filter 'health_percent<=50' | damage amount=2
  • Only sources may appear inside union [ ]; an action or gate reports union_only_sources;
  • A source that matches nothing does not fail the whole union — only all-empty yields an empty flow;
  • Merging uses a LinkedHashSet (an entity selected by two sources appears once);
  • union passes validation while the old multiple_sources diagnostic still rejects "two bare source stages" — seeing that error means switch to union

Compile-time branch (parse_if) ​

if evaluates at runtime; parse_if evaluates at compile time — when the condition is false the whole block never enters the compiled output, saving both parsing and runtime checks in long nested pipelines:

text
parse_if true [ send_message text="this block is compiled in" ]
parse_if false [ send_message text="discarded at load time" ]
  • The condition must be statically evaluable; a %...% placeholder reports parse_if_placeholder (that is a runtime concern — use if);
  • A condition the engine cannot resolve to a boolean reports parse_if_not_evaluable;
  • When the condition is false the block is skipped at parse time, without shifting later tokens' columns

Error control flow (on_error and retry) ​

on_error [ ... ] routes a failed action into a handler; retry= / retry_delay= replays a stage with backoff after failure:

text
damage amount=3 on_error [ send_message text="<red>Damage failed: %var.error%</red>" ]
take_item item=sword amount=1 retry=3 retry_delay=1s
  • %var.error% expands to one of the seven failure kinds (INVALID_CONFIG / MISSING_CONTEXT / WRONG_THREAD / TIMEOUT / OWNER_DISABLED / INTERNAL_ERROR / REJECTED); %var.error_reason% carries the detailed reason key;
  • A handler cannot nest another on_error (caught at compile time as nested_on_error; split into several pipelines for deeper handling);
  • retry counts are capped by action.pipeline.max_retry_times; retry_delay accepts 1s / 20t forms;
  • Parentheses ( ) after on_error do nothing — the handler must be wrapped in [ ]

Compile limits ​

These limits are checked at load time. Exceeding one rejects that config entry and keeps the last valid config instead of silently truncating — a truncated times=100000 looks like it worked, which is harder to diagnose than an error. Shipped defaults:

yaml
action:
  pipeline:
    max_repeat_times: 100
    max_sequence_depth: 8
    max_branch_depth: 16
    max_retry_times: 5
FieldTypeRequiredDefaultRangeDescription
action.pipeline.max_repeat_timesintegerNo100positive integer (<=0 falls back)Upper bound for every <interval> times <n>; exceeding it rejects the entry
action.pipeline.max_sequence_depthintegerNo8positive integer (<=0 falls back)Maximum run nesting depth
action.pipeline.max_branch_depthintegerNo16positive integer (<=0 falls back)Maximum nesting depth shared by if, weight and on_error
action.pipeline.max_retry_timesintegerNo5positive integer (<=0 falls back)Upper bound for a stage's retry= count; exceeding it reports retry_limit_exceeded

Example: allow up to 500 repeats and deeper weight nesting

yaml
action:
  pipeline:
    max_repeat_times: 500
    max_branch_depth: 24

Common parameters (apply to every stage) ​

The three entries below are not declared by any individual stage; the compiler strips them out of the stage's argument list during parsing, so they work on source, gate, and action alike, and you can append them after the stage name as needed:

ParameterTypeDefaultPurpose
on_error [ ... ]branch bodynoneRuns the branch body (a full pipeline) when this stage fails; the body reads %var.error% (one of seven failure kinds) and %var.error_reason% (the detailed reason key). Nesting on_error inside the body is rejected at compile time (nested_on_error)
retry=INTEGERno retryReplay count after a failure, capped by action.pipeline.max_retry_times (default 5); exceeding it raises retry_limit_exceeded. A literal must be a non-negative integer, otherwise it parses as -1 and triggers a diagnostic
retry_delay=DURATIONimmediateBackoff delay before each replay; accepts 20t / 1s / 500ms. Negative values are treated as 0. Has no effect without retry=

Usage rules:

  • Decorators sit in the argument area after the stage name; the compiler strips them before validating the stage's own parameters, so they never raise unknown_argument. Example: damage amount=3 on_error [ send_message text="failed" ], take_item item=sword amount=1 retry=3 retry_delay=1s
  • Bracket syntax: the branch body must be paired [ ]; the on_error ( ... ) form with parentheses is invalid
  • Stage and parameter names are lowercased: Send_Message equals send_message, AMOUNT=3 equals amount=3
  • Two forms: key=value, or a bare value (positional) matched against the stage's declared order — chance 25% is the same as chance chance=25%
  • Quoting and escapes: quote values containing spaces, MiniMessage tags, |, [, ], : or #. Single and double quotes both work; inside quotes \n, \t, \\, \", \' are escapes and a quoted | never splits the pipeline
  • Placeholders resolve late: %var.*% / PAPI placeholders render right before execution, so non-placeholder literals are type-checked at compile time while placeholder-bearing values defer the check to runtime; a mismatch raises invalid_argument_type
  • Undeclared means rejected: a parameter name the stage did not declare raises unknown_argument (with candidate suggestions); a missing required parameter raises missing_required_argument. Run <any plugin root> debug action check "<pipeline line>" to get every diagnostic before loading

Each stage's dedicated parameters are documented in its section below; for the id list and jump targets see the Stage ID table. Full type definitions and examples are in Parameter types

Loop task limits ​

Long-running loops started by start_task are governed separately by action.loop.*, independently of the compile limits above. Shipped defaults:

yaml
action:
  loop:
    enabled: true
    min_sync_interval: 5t
    min_async_interval: 100ms
    max_times: 7200
    max_active_loops_total: 5000
    max_active_loops_per_player: 16
    max_active_loops_per_plugin: 1000
    cancel_player_loops_on_quit: true
    cancel_plugin_loops_on_disable: true
FieldTypeRequiredDefaultRangeDescription
action.loop.enabledbooleanNotruetrue / falseWhether loop tasks may start at all
action.loop.min_sync_intervaldurationNo5t>=1t (precheck)Minimum interval for sync-domain loops
action.loop.min_async_intervaldurationNo100ms>=1t (precheck)Minimum interval for async-domain loops
action.loop.max_timesintegerNo7200>0 (precheck)Maximum executions for a single loop task
action.loop.max_active_loops_totalintegerNo5000>0 (precheck)Server-wide cap on active loop tasks
action.loop.max_active_loops_per_playerintegerNo16>0 (precheck)Per-player cap on active loop tasks
action.loop.max_active_loops_per_pluginintegerNo1000>0 (precheck)Per-plugin cap on active loop tasks
action.loop.cancel_player_loops_on_quitbooleanNotruetrue / falseCancel a player's loop tasks when they quit
action.loop.cancel_plugin_loops_on_disablebooleanNotruetrue / falseCancel a plugin's loop tasks when it is disabled

Both interval keys are time values (t/s/ms); the precheck requires at least 1 tick and reports an error otherwise. The loops themselves are created and cancelled by the start_task / stop_task stages (see Long-running tasks)

Example: raise the per-plugin loop cap and let loops survive a player quitting

yaml
action:
  loop:
    max_active_loops_per_plugin: 2000
    cancel_player_loops_on_quit: false

Parameter types ​

Stage parameters carry the types listed below, and the parameter tables below name them. Types are validated at load time, so a value you write wrong does not wait until runtime to surface

TypeMeaningExample
STRINGText. Quote it when it contains spaces or special characterstext="<green>Success"
INTEGERWhole numberamount=3, count=10
DOUBLEDecimal numbervolume=0.8, amount=25.5
BOOLEANtrue or falsedelete_item=false, icon=true
DURATIONTime. Accepts t (ticks), s, ms; a bare number means ticks20t, 1s, 500ms
PERCENTAGEProbability. Accepts percentages, decimals, and fractions50%, 0.5, 1/3
ENTITY_TYPEA Bukkit EntityType nametype=zombie
MATERIALA Bukkit Material namematerial=stone
SOUNDA sound key: a Bukkit Sound enum name or namespace:keysound=minecraft:block.anvil.use
EXPRESSIONArithmetic evaluated by the CoreLib expression engineamount=%var.level%*4+18

Migrating from the old syntax ​

If you still have old config, three rules cover most of the rewrite:

  • Stage ids are underscore-separated. sendmessage becomes send_message, givemoney becomes give_money, runcommandasconsole becomes run_command_as_console, and so on for every id
  • @ control prefixes became stages in the pipeline. @chance=25% becomes chance 25%, @delay=20t becomes after 20t, and @if=<condition> becomes an if <condition> [ ... ] branch. @ignore_failure has no replacement: failure semantics now belong to each stage, so where you need "failure must not block", use a branch or move that stage onto its own line
  • Write the source. Old action lines implicitly acted on the player from the event. A pipeline states its source: self for the caster (also the default when omitted), inherited for the targets your caller passed in

A few ids changed by more than an underscore:

OldNow
raysource looking_at
loopsync / loopasyncstart_task (sync/async is no longer a config option; see that stage)
cancelloopstop_task
@template=name / usetemplaterun name
killentity radius=... type=...source nearby does the search, kill_entity only removes
createitem id=... senditem id=...create_item publishes the single pipeline item, send_item reads it; no id
castmythicskillcast_mythic_skill, provided by EmakiSkills

cast_skill carries one breaking semantic change: its skill argument is now an EmakiSkills skill id, no longer a MythicMobs skill id. Old config written as cast_skill skill=<mythic skill> must become cast_mythic_skill skill=<mythic skill>, otherwise it fails looking for an EmakiSkills skill that does not exist

Built-in sources ​

A source produces the target flow and usually opens the pipeline. Omitting the source is equivalent to self

self ​

The caster. No parameters

text
self | send_message text="<green>Done</green>"

inherited ​

The target flow handed in by the caller or by the previous phase. No parameters

You have to write this one out: omitting the source falls back to self, so a pipeline that meant to act on inherited targets silently acts on the caster instead

Pairs with keep: an earlier phase writes looking_at | keep to record the flow, and a later phase reads it back with inherited

text
inherited | damage amount=8

trigger ​

The entity a trigger named, for triggers whose subject differs from the caster. No parameters

The context holds a name or UUID rather than an entity reference. Pipelines may be compiled long before they run, and parking live entities in context keys gets in the way of collection

text
trigger | send_message text="<yellow>You set off the trap</yellow>"

origin ​

The pipeline's spatial reference point, as a location target. No parameters

text
origin | spawn_particle particle=flame count=20

looking_at ​

The entity under the caster's crosshair

ParameterTypeRequiredDefaultDescription
rangeDOUBLENo5Ray length
widthDOUBLENo0.5Ray width
text
looking_at range=12 | damage amount=6
looking_at | keep | send_message text="<red>You are locked on</red>"

nearby ​

Entities around the pipeline reference point

ParameterTypeRequiredDefaultDescription
radiusDOUBLENo1Search radius
limitINTEGERNo1Maximum entities returned
typeENTITY_TYPENo""Entity type filter; empty means no filter
include_playersBOOLEANNofalseWhether players count as candidates

Results are sorted by distance from the reference point, then truncated to limit. Finding no entity is "empty" and skips the following stages; naming an entity type that does not exist is "invalid" and raises an error. The old implementation treated a typo'd type as a skip, which meant server owners never saw their own mistake

text
nearby radius=8 limit=5 type=zombie | damage amount=10
nearby radius=4 limit=20 include_players=true | give_potion_effect type=slowness level=1 duration=5s

nearby_players ​

Players around the pipeline reference point

ParameterTypeRequiredDefaultDescription
radiusDOUBLENo1Search radius
limitINTEGERNo0Maximum returned; 0 means all

Same filtering logic as nearby, restricted to players. limit defaults to 0 (all) rather than 1, because party-wide and area-wide effects normally want every player, not the closest one

text
nearby_players radius=10 | send_message text="<gold>Your party gained a buff</gold>"

offset ​

A location offset from the pipeline reference point

ParameterTypeRequiredDefaultDescription
xDOUBLENo0X offset
yDOUBLENo0Y offset
zDOUBLENo0Z offset
relativeBOOLEANNofalseOffset along the reference point's own facing

With relative=false the offset follows world axes. With relative=true it follows the reference point's facing, where z is forward and x is right

text
offset y=2 | spawn_particle particle=end_rod count=30
offset z=3 relative=true | explosion power=2 break_blocks=false

at ​

An absolute coordinate, or one relative to the reference point

ParameterTypeRequiredDefaultDescription
worldSTRINGNo""World name; empty uses the reference point's world
xSTRINGNo~X coordinate, supports ~
ySTRINGNo~Y coordinate, supports ~
zSTRINGNo~Z coordinate, supports ~

The coordinates are STRING rather than DOUBLE so that ~ notation keeps working: ~ is "the reference point's value on this axis" and ~5 is "five more than that", matching vanilla command coordinate syntax

text
at world=world x=100 y=65 z=-30 | spawn_particle particle=end_rod count=50
at x=~ y=~-1 z=~ | set_block material=oak_planks

player_by_name ​

An online player identified by name or UUID

ParameterTypeRequiredDefaultDescription
nameSTRINGYes—Player name or UUID

name is positional, so name= can be dropped

text
player_by_name Notch | send_message text="<yellow>You were called out</yellow>"
player_by_name %var.winner% | give_money amount=500

select ​

Picks targets by a configured selector definition; see Target selectors below

ParameterTypeRequiredDefaultDescription
nameSTRINGYes—Selector id defined under action.selectors

name is positional, so name= can be dropped

text
select nearby_elites | damage amount=5
select nearby_elites | play_sound sound=minecraft:entity.zombie.hurt volume=1 pitch=1

all_players ​

All online players on the server. Execution domain SERVER_GLOBAL

ParameterTypeRequiredRangeDefaultDescription
limitINTEGERNointeger0Max players; 0 means no limit
text
all_players | send_message text="example"

dropped_items ​

Dropped items around the pipeline origin. Execution domain LOCATION_REGION

ParameterTypeRequiredRangeDefaultDescription
radiusDOUBLENofinite number8Search radius
limitINTEGERNointeger0Max dropped items; 0 means no limit
text
dropped_items | send_message text="example"

faction_members ​

Online players in the given faction roster. Execution domain SERVER_GLOBAL

ParameterTypeRequiredRangeDefaultDescription
memberSTRINGYesstring—Comma-separated list of member player names
text
faction_members member=player1, player2 | send_message text="example"

highest_block ​

The highest block in the origin's column. Execution domain LOCATION_REGION

This stage has no dedicated parameters

text
highest_block | send_message text="example"

op_players ​

Online OP players on the server. Execution domain SERVER_GLOBAL

This stage has no dedicated parameters

text
op_players | send_message text="example"

party ​

Online players holding the same party permission node. Execution domain SERVER_GLOBAL

ParameterTypeRequiredRangeDefaultDescription
permissionSTRINGYesstring—Party permission node, e.g. emaki.party.alpha
text
party permission=emaki.party.alpha | send_message text="example"

players_by_gamemode ​

Online players in the given game mode. Execution domain SERVER_GLOBAL

ParameterTypeRequiredRangeDefaultDescription
gamemodeSTRINGYessurvival / creative / adventure / spectator—Game mode, e.g. survival / creative / adventure / spectator
limitINTEGERNointeger0Max players; 0 means no limit
text
players_by_gamemode gamemode=survival | send_message text="example"

players_in_region ​

Players inside the cube centered on the pipeline origin. Execution domain LOCATION_REGION

ParameterTypeRequiredRangeDefaultDescription
widthDOUBLENofinite number16Half width on the X axis
heightDOUBLENofinite number16Half height on the Y axis
depthDOUBLENofinite number16Half depth on the Z axis
limitINTEGERNointeger0Max players; 0 means no limit
text
players_in_region | send_message text="example"

players_with_material ​

Online players carrying an item of the given material. Execution domain SERVER_GLOBAL

ParameterTypeRequiredRangeDefaultDescription
materialMATERIALYesBukkit Material name—Item material
limitINTEGERNointeger0Max players; 0 means no limit
text
players_with_material material=diamond | send_message text="example"

random ​

A random sample of online players. Execution domain SERVER_GLOBAL

ParameterTypeRequiredRangeDefaultDescription
countINTEGERNointeger1Number of targets to sample
text
random | send_message text="example"

region ​

Entities in the cube whose corner is the pipeline origin. Execution domain LOCATION_REGION

ParameterTypeRequiredRangeDefaultDescription
widthDOUBLENofinite number8Half width on the X axis
heightDOUBLENofinite number8Half height on the Y axis
depthDOUBLENofinite number8Half depth on the Z axis
limitINTEGERNointeger0Max entities; 0 means no limit
typeENTITY_TYPENoBukkit EntityType name``Entity type filter
include_playersBOOLEANNotrue / falsetrueWhether players are eligible targets
text
region | send_message text="example"

ring ​

Entities between the inner and outer radius. Execution domain LOCATION_REGION

ParameterTypeRequiredRangeDefaultDescription
min_radiusDOUBLENofinite number1Inner radius
max_radiusDOUBLENofinite number8Outer radius
limitINTEGERNointeger0Max entities; 0 means no limit
typeENTITY_TYPENoBukkit EntityType name``Entity type filter
include_playersBOOLEANNotrue / falsetrueWhether players are eligible targets
text
ring | send_message text="example"

vertical_block ​

Block position offset vertically from the origin. Execution domain LOCATION_REGION

ParameterTypeRequiredRangeDefaultDescription
offsetINTEGERNointeger1Blocks to move up (+) or down (-)
text
vertical_block | send_message text="example"

world_players ​

Online players in the given world. Execution domain SERVER_GLOBAL

ParameterTypeRequiredRangeDefaultDescription
worldSTRINGNostring``World name; empty uses the origin's world
limitINTEGERNointeger0Max players; 0 means no limit
text
world_players | send_message text="example"

Target selectors ​

A selector stores three decisions in configuration — which source picks the targets, which conditions they must satisfy, and how many are kept — so an action line only names it. You do not need one to filter targets: a single action line can already carry its own conditions through filter. A selector exists to pull the reusable part into the main configuration and keep it consistent across action lines

Two ways to use it ​

The same filtering task can be written straight into one action line, or defined once as a selector and then called. The inline form needs no configuration at all

FormHow it is writtenWhen to use it
Inline filtera source stage + the filter or where gateOne-off conditions you want to write and tweak on the spot, without touching the main config
Named selectoraction.selectors.<id> in the main config + select <id>One condition set reused by several action lines, or changed once for everything
Mixedselect <id> with inline argument overrides, then an extra filterReuse a baseline condition set and append conditions for this one action

All three forms below perform the same task: deal 5 damage to every zombie within 16 blocks whose health is at or below 50%

Inline form (no pre-configuration) ​

The source, the filtering gate, and the action all sit on one line. Entity type goes to the source's own type parameter; the health ratio goes to filter's predicate string:

text
nearby radius=16 limit=5 type=zombie | filter 'health_percent<=50' | damage amount=5

You can also fold the type condition into the predicate string and let filter do all the narrowing:

text
nearby radius=16 limit=5 | filter 'entity_type=ZOMBIE health_percent<=50' | damage amount=5

filter's positional argument must be quoted. Inside the predicate string a space means AND, || means OR, a leading ! means NOT, parentheses group, and a comma separates values. The full rule set and every accepted key are under filter

If you would rather not learn the predicate syntax, use where with an expression; it is evaluated per target after %target_*% placeholders are expanded:

text
nearby radius=16 limit=5 type=zombie | where %target_health_percent%<=50 | damage amount=5

where takes exactly one expression, so multiple dimensions have to be chained with &&, which reads worse than a predicate string

Named selector form (configure first, then call) ​

Put the same conditions into action.selectors.<id> in the main config:

yaml
action:
  selectors:
    low_health_zombies:
      source: nearby
      arguments:
        radius: 16
        limit: 5
        type: zombie
      condition:
        predicates: 'health_percent<=50'

The action line is then just the name:

text
select low_health_zombies | damage amount=5

Mixed form (reuse plus inline additions) ​

The selector supplies the baseline; the action line overrides the referenced source's arguments, or adds one-off conditions with filter / where:

text
select low_health_zombies radius=24
select low_health_zombies radius=24 | filter 'level>=30'

Inline arguments must be declared by the referenced source stage (nearby declares radius, limit, type, include_players); a misspelled name returns unknown_argument instead of being silently ignored. A selector's own limit can only be set in the configuration

Choosing between them ​

Both forms share one evaluation stack: a filter predicate string and a selector's condition.predicates are exactly the same syntax, and the predicate keys are the same names as the typed nodes in condition.entries

  • The condition is used once, or you want to tune it for this one action → inline filter / where
  • The same condition set appears in recipes, GUIs and triggers, and you want one place to change it → a named selector
  • You need the at_least / exactly counting modes, or invalid_as_failure: false (let undecidable targets through) → define a selector in the main config; filter is fixed to exclude anything undecidable

Defining a selector ​

Selectors live under action.selectors.<id> in the CoreLib main configuration:

yaml
action:
  selectors:
    nearby_elites:
      source: nearby
      arguments:
        radius: 16
        limit: 5
        include_players: false
      limit: 2
      condition:
        type: all_of
        invalid_as_failure: true
        entries:
          - type: entity_type
            value: [ZOMBIE, SKELETON]
          - type: health_percent
            op: "<="
            value: 50
FieldTypeRequiredDefaultRangeDescription
sourcestringYes—a registered source id (never select itself)Source id to run, including sources registered by other modules
argumentssectionNo{}scalar valuesArguments for that source, written like action-line arguments; scalars only
limitintegerNo0non-negative integer; 0 means no truncationTruncates after filtering; 0 keeps everything
conditionsectionNo{}a condition group (type / entries / required_count / predicates / invalid_as_failure)See the condition group syntax below; without it every selected target is kept

source may not be select itself; that is rejected at load time. The selector inherits the referenced source's thread domain: referencing nearby still runs in the location region and never reads across regions

Action lines call it as select <selector id>, and selector ids are case-insensitive. Arguments of the referenced source can be overridden inline — write only what changes; everything else comes from the main configuration:

text
select low_health_elites
select low_health_elites radius=24 limit=8
select low_health_elites radius=24 | filter 'level>=30'

Inline arguments must be declared by the referenced source; a misspelled name answers unknown_argument instead of being silently ignored. The selector's own limit stays configuration-only. To stack more conditions, append filter or where rather than going back to the main configuration

Condition group syntax ​

condition matches the condition system: entries lists condition nodes, type selects the combination, and required_count drives the counting modes

A condition can also be written as a one-line predicate string (exactly the filter grammar); use the predicates field instead of entries:

yaml
condition:
  predicates: 'entity_type=ZOMBIE health_percent<=50'
  invalid_as_failure: false

predicates and entries are mutually exclusive; writing both is rejected at load time rather than silently ignoring one of them. Use predicates for simple conditions and structured entries when you need at_least/exactly or deep nesting

CombinationMeaningAlias
all_ofEvery entry must holdand
any_ofAt least one entry must holdor
none_ofNo entry may holdnot
at_leastAt least required_count hold—
exactlyExactly required_count hold—

Write an object containing entries inside entries to nest a group; the nested group declares its own type:

yaml
condition:
  type: all_of
  entries:
    - type: any_of
      entries:
        - type: permission
          value: emaki.admin
        - type: level
          op: ">="
          value: 30
    - type: none_of
      entries:
        - type: mythicmobs
          id: SkeletonKing

Condition nodes ​

Nodes come in four kinds: expression nodes (a bare string or type: expression), built-in typed nodes, registered mob-system nodes, and third-party registered types. Resolution order is built-ins, then mob systems, then third-party conditions

Built-in typed nodes:

typeTested valueFields
entity_typeEntity typevalue: type name or list; any match passes
playerWhether it is a playervalue: true / false
deadWhether it is deadvalue: true / false
worldCurrent worldvalue: world name or namespace:key
healthCurrent healthop + value
health_percentHealth as a percentageop + value
distanceDistance from the originop + value
levelPlayer experience levelop + value
foodPlayer food levelop + value
gamemodePlayer game modevalue: survival / creative / adventure / spectator
permissionPlayer permissionsvalue: node or list; any match passes
potion_effectActive effectseffect: effect name or list; min_amplifier defaults to 0
scoreboard_tagScoreboard tagsvalue: tag or list
stateEntity boolean statekey: state name; value: expected result, default true

op is required for numeric nodes and accepts ==, !=, <, <=, >, >=. Valid state keys are sneaking, sprinting, flying, gliding, swimming, in_water, on_ground; sprinting and flying are answerable for players only

Mob-system nodes use the system id as type, id to filter mob ids (a list is allowed), and op + level to filter levels (a system without levels always reports 0):

yaml
condition:
  entries:
    - type: mythicmobs
      id: [SkeletonKing, ZombieLord]
      op: ">="
      level: 5
    - type: emakimobs
      id: elite_zombie

mythicmobs is recognised by EmakiCoreLib itself; emakimobs is registered by EmakiMobs when it enables. Any other mob system can contribute its identifier through EmakiCoreLibApi.registerTargetIdentityProvider(plugin, provider). When a system is not registered the node is unanswered; when it is registered but the entity does not belong to it, the node is simply not satisfied

Expression nodes and target placeholders ​

Expression nodes work as usual and additionally receive %target_-prefixed placeholders:

PlaceholderMeaning
%target_type%Entity type name
%target_name%Entity name
%target_world%World name
%target_health%Current health
%target_max_health%Maximum health
%target_health_percent%Health as a percentage
%target_distance%Distance from the pipeline origin
%target_level% / %target_food% / %target_gamemode%Player experience level, food level, game mode
%target_permission_<node>%Whether the player holds that permission
%target_tag_<tag>%Whether the scoreboard tag is present
%target_state_<state>%Whether that state holds; unchanged when the state does not apply to the target
%target_effect_<effect>%Effect amplifier (0 is level I), empty when the effect is absent
%target_<system>_id%Mob-system id, such as %target_mythicmobs_id%
%target_<system>_level%Mob-system level

Placeholders that cannot be answered stay as written, so the expression fails to evaluate and follows the rules below

yaml
condition:
  entries:
    - '%target_health_percent% <= 30 && %target_distance% <= 8'

Unanswered conditions and invalid_as_failure ​

A condition node answers one of three ways: satisfied, not satisfied, or unanswered. Unanswered typically means a player-only condition on a non-player, an absent mob system, a numeric node missing op or value, or an expression that will not evaluate

invalid_as_failure decides how an unanswered node joins the combination, exactly as in the condition system:

  • true (default): any unanswered node fails the whole group, so the target is dropped. This is the safe direction — a mistyped configuration excludes targets instead of letting the wrong ones through
  • false: unanswered nodes are skipped and do not count toward the required amount; when every node is skipped the group counts as satisfied

Examples ​

Low-health elites:

yaml
action:
  selectors:
    low_health_elites:
      source: nearby
      arguments:
        radius: 20
        limit: 10
      condition:
        type: all_of
        entries:
          - type: entity_type
            value: [ZOMBIE, HUSK, DROWNED]
          - type: health_percent
            op: "<="
            value: 40
          - type: state
            key: on_ground

Nearby players that qualify for a reward:

yaml
action:
  selectors:
    reward_candidates:
      source: nearby_players
      arguments:
        radius: 32
      limit: 5
      condition:
        type: all_of
        entries:
          - type: any_of
            entries:
              - type: permission
                value: emaki.vip
              - type: level
                op: ">="
                value: 30
          - type: none_of
            entries:
              - type: gamemode
                value: creative

MythicMobs and EmakiMobs identities:

yaml
action:
  selectors:
    boss_like:
      source: nearby
      arguments:
        radius: 24
      condition:
        type: any_of
        entries:
          - type: mythicmobs
            op: ">="
            level: 10
          - type: emakimobs
            id: bone_lord

Used from an action line:

text
select low_health_elites | kill_entity
select reward_candidates | give_money amount=100

Load-time checks ​

Config precheck validates selector structure and reports the offending path (for example action.selectors.low_health_elites.condition.entries[1]): a missing source, source pointing at select itself, a source that is not currently registered, a numeric node missing op or value, and an unknown state name. The first two are errors, which fail the precheck and keep the previous working configuration; the rest are warnings

At runtime, a node type that is neither built-in, a registered system, nor a registered third-party condition logs one warning listing the available types and counts as unanswered

Third-party condition types ​

Modules and other plugins can register their own condition types and use them inside condition.entries:

java
EmakiCoreLibApi.registerTargetCondition(this, new CoreTargetCondition() {
    @Override
    public String id() {
        return "myplugin_custom";
    }

    @Override
    public CoreTargetOutcome test(CoreActionSubject subject,
            CoreStageContext context,
            CoreTargetConditionArguments arguments) {
        return subject.entityOrNull() == null
            ? CoreTargetOutcome.UNKNOWN
            : CoreTargetOutcome.PASS;
    }
});

Built-in ids cannot be shadowed — a colliding registration is refused with a stable reasonKey. Close the handle when the plugin disables; this registry is not rebuilt by a CoreLib reload

Built-in gates ​

A gate filters the target flow, controls timing, or writes values into the pipeline context; gates produce no effects of their own

where ​

Drops every target whose condition does not hold

ParameterTypeRequiredDefaultDescription
conditionSTRINGYes—Boolean condition

condition is positional. Pipeline placeholders are substituted first and the expression is then evaluated per target, so it can use %var.<name>%, CoreLib's player placeholders, variables written by the calling module and PlaceholderAPI placeholders — plus the %target_*% placeholders listed under Target selectors

When the condition contains no %target_*% placeholder every target gets the same verdict, so the flow is kept or cleared as a whole — existing configurations behave exactly as before. Only a condition that reads target placeholders filters target by target

text
looking_at | where %target_health_percent% < 10 | kill_entity
nearby radius=6 limit=10 | where %player_world%=="world" | damage amount=4

For anything more complex use the filter predicates; put conditions shared by several actions into action.selectors

filter ​

Filters targets by inline predicates, without defining a selector in the main configuration first. Its positional argument is one quoted predicate string:

ParameterTypeRequiredDefaultDescription
conditionSTRINGYes—The predicate string

Predicate grammar:

RuleFormMeaning
Conjunctionentity_type=ZOMBIE health_percent<=50Whitespace between items means AND
Disjunctionlevel>=30 || permission=emaki.vip|| means OR
Negation!gamemode=creativeLeading ! means NOT
Grouping(health<=30 || level>=50) entity_type=ZOMBIEParentheses nest freely
Value listentity_type=ZOMBIE,SKELETONComma means "any of"; with !=, "none of"

Available keys carry the same names as the action.selectors condition types, with no aliases:

KeyFormMeaning
entity_typeentity_type=ZOMBIE,SKELETONEntity type
player / deadplayer=true, dead=falseWhether it is a player / is dead
worldworld=world_netherWorld name or namespace:key
healthhealth<=10Current health
health_percenthealth_percent<=50Health as a percentage (0–100)
distancedistance<=8Distance from the pipeline origin
level / foodlevel>=30Player experience level / food level
gamemodegamemode!=creativePlayer game mode
permissionpermission=emaki.vipPermission node; a list means "any of"
scoreboard_tagscoreboard_tag=eliteScoreboard tag
state.<name>state.sneaking, state.on_ground=falseEntity boolean state, expected true by default
potion_effect.<name>potion_effect.speed>=2Effect amplifier (0 is level I); >=0 means "has it"
<mob system id>mythicmobs=SkeletonKing, emakimobs=elite_zombieMob-system identity; a list is allowed
<mob system id>_levelmythicmobs_level>=5Mob-system level

Numeric keys require one of ==, !=, <, <=, >, >= — writing health without an operator is an error rather than a silent pass. The predicate string must be quoted: unquoted, entity_type=ZOMBIE is read as a stage argument and rejected as unknown. That failure is deliberate, not an inconvenience

text
nearby radius=12 | filter 'entity_type=ZOMBIE health_percent<=50' | damage amount=5
select low_health_elites | filter 'level>=30 || permission=emaki.vip' | give_money amount=100

filter and where stack, filtering in pipeline order. Evaluation shares the same machinery as action.selectors condition groups; filter always behaves as invalid_as_failure: true, so an unanswered predicate (a player-only condition on a non-player, an absent mob system, an expression that will not evaluate) drops that target. When you need unanswered to pass instead, define a selector with invalid_as_failure: false

chance ​

Ends the pipeline when the roll fails

ParameterTypeRequiredDefaultDescription
chancePERCENTAGEYes—Probability, such as 50%, 0.5, 1/3

chance is positional. A failed roll reports "stopped", not "invalid" — losing the roll is the whole point of the stage. chance abc, on the other hand, is a typo the server owner needs to see as an error

When something should always happen but which outcome is decided by odds, use weighted branches instead of stacking chance lines — separate lines are judged independently, so none may fire, or several may fire at once

text
self | chance 10% | broadcast_message text="<gold>%player_name% hit a rare reward!</gold>"
self | chance 1/3 | give_money amount=100 provider=vault

limit ​

Keeps the first count targets in the flow, preserving order

ParameterTypeRequiredDefaultDescription
countINTEGERYes—How many targets to keep

count is positional

text
nearby radius=15 limit=50 | sort_by distance | limit 3 | damage amount=12

sort_by ​

Sorts the target flow by distance or health

ParameterTypeRequiredDefaultDescription
keySTRINGYes—distance or health
orderSTRINGNoascasc or desc

key is positional. Combined with limit this expresses "the three closest" or "the one with the least health"

text
nearby radius=20 limit=30 | sort_by health order=asc | limit 1 | kill_entity
nearby_players radius=25 | sort_by distance order=desc | limit 5 | send_title title="<red>Too far from the fight</red>"

set ​

Writes pipeline variables, read back as %var.<name>%

This stage declares no parameters, because the writer of the pipeline chooses the names — CoreLib cannot know in advance about a variable named by set damage=%var.level%*4+18. Load-time validation skips the unknown-parameter check for this stage

Values that evaluate as arithmetic are stored as numbers, so %var.damage% reads back as 22 rather than %var.level%*4+18. Everything else is stored verbatim

text
self | set damage=%var.level%*4+18 | set label=Critical | send_message text="<red>%var.label% %var.damage%</red>"

keep ​

Marks the current target flow as the one to carry into the next phase. No parameters

The flow already passes between stages inside a pipeline, so this gate passes it through untouched. What makes it more than a no-op is that the interpreter records the flow it sees, and that recording is how one phase hands its targets to the next. A skill script writes looking_at | keep in its cast phase and reads it back with inherited in its hit phase

It records the flow as of the moment it runs, not the pipeline's final flow, so where the line sits within the phase does not change the result: later gates narrowing the flow do not alter what was recorded, and a second keep overwrites the first

text
looking_at | keep | send_message text="<gray>Target locked</gray>"

stop ​

Ends the pipeline here. No parameters

It reports "stopped", so the pipeline result is a skip rather than a failure — deliberately stopping is not an error. Mostly used inside branches

text
looking_at | if %var.dead% [ stop ] else [ damage amount=5 ]

create_item ​

Builds an item and publishes it as the pipeline's item value

ParameterTypeRequiredDefaultDescription
item_sourceSTRINGNo""Item source
amountINTEGERNo1Item amount

It is registered as a gate rather than an action because writing typed context is a gate's job: only a gate's passed result is written back into the pipeline context. That is also why it passes the target flow through unchanged — the stage adds a value, it does not consume targets

The pipeline holds a single item key, so there is nothing to name and the old id parameter is gone. A second create_item in the same pipeline replaces the first one's value

item_source uses the CoreLib hyphenated item source shorthand — see Item Sources — for example vanilla minecraft-diamond or custom emakiitem-flame_blade. Vanilla sources do not accept colon notation such as minecraft:diamond

text
self | create_item item_source=minecraft-golden_apple amount=2 | send_item
self | create_item item_source=emakiitem-flame_sword | send_item

after ​

Delays every stage after it

ParameterTypeRequiredDefaultDescription
delayDURATIONYes—Delay, such as 10t, 500ms, 2s

delay is positional. This is a timing stage rather than a flow transform: the interpreter recognizes after, treats the rest of the pipeline as its body, and schedules that body with the delay. The body re-validates caster, targets, and owning plugin before it runs, so a target that disappeared during the wait produces a skip rather than an error

text
self | after 1s | send_message text="<gray>Sent one second later"
self | send_message text="<gold>Ready</gold>" | after 20t | send_message text="<red>Go!</red>"

every ​

Repeats every stage after it on an interval

ParameterTypeRequiredDefaultDescription
intervalDURATIONNo1tInterval, such as 20t or 1s
timesINTEGERNo0Extra runs after the first

Written as every <interval> times <n>. times counts runs beyond the first, so times 0 (the default) means the body runs once. The count is bounded by action.pipeline.max_repeat_times (default 100), and exceeding it rejects the config entry

text
self | every 10t times 5 | spawn_particle particle=flame count=10
origin | every 1s times 3 | play_sound sound=minecraft:block.note_block.bell volume=1 pitch=1.5

every suits short bursts inside one pipeline. For a long-running loop you can cancel by key, use start_task

distance_filter ​

Keep only targets within [min, max] distance of the origin. Thread requirement NEEDS_ENTITY_READ

ParameterTypeRequiredRangeDefaultDescription
minDOUBLENofinite number0Minimum distance
maxDOUBLENofinite number32Maximum distance
text
self | distance_filter | send_message text="example"

sample ​

Randomly sample count targets. Thread requirement PURE

ParameterTypeRequiredRangeDefaultDescription
count (positional)INTEGERYesinteger—Number of targets to sample
text
self | sample '3' | send_message text="example"

shuffle ​

Randomly shuffle target order. Thread requirement PURE

This stage has no dedicated parameters

text
self | shuffle | send_message text="example"

where_all ​

Keep targets satisfying every predicate; predicates separated by ||. Thread requirement NEEDS_ENTITY_READ

ParameterTypeRequiredRangeDefaultDescription
conditions (positional)STRINGYesstring—Multiple predicates separated by `
text
self | where_all 'player || health_percent<=50' | send_message text="example"

where_any ​

Keep targets satisfying any predicate; predicates separated by ||. Thread requirement NEEDS_ENTITY_READ

ParameterTypeRequiredRangeDefaultDescription
conditions (positional)STRINGYesstring—Multiple predicates separated by `
text
self | where_any 'health_percent<=50 || dead' | send_message text="example"

Built-in actions ​

Actions are the stages that produce effects. The "target requirement" noted for each stage says what kind of target flow it needs; when the requirement is not met the stage is skipped, without failing the whole pipeline

  • NONE: needs no target at all
  • OPTIONAL: runs either way, and still runs once with zero targets
  • REQUIRED_ENTITY: needs at least one entity target
  • REQUIRED_LOCATION: needs at least one location target
  • REQUIRED_ANY: needs at least one target, entity or location

Validation and auditing for numeric stages ​

Health, food, experience, money, and item-amount stages validate amount explicitly instead of clamping silently:

  • An invalid value fails with INVALID_CONFIG and carries that value in the message, so the problem is visible to the server owner rather than looking like a config that "mostly works"
  • Health, food, experience, and money stages require a finite, non-negative amount (NaN and Infinity are invalid). Decrease stages (take_exp, take_money, take_item, take_food) and item-granting stages (give_item, set_item, drop_item) additionally require it to be greater than 0
  • Insufficient funds, experience, items, or food return REJECTED with the required and current amounts. Nothing is partially withdrawn, and a balance or count never goes negative
  • Every successful or failed numeric change is written to the CoreLib DebugLogger module action.audit, with stage, operation, target, before/after, amount, source plugin, and phase; failure lines also carry reason and details. See the global debug switch in Commands and permissions

Messages and feedback ​

send_message ​

Sends a MiniMessage chat message. Target requirement REQUIRED_ENTITY

ParameterTypeRequiredDefaultDescription
textSTRINGYes—Message text in MiniMessage
yaml
actions:
  # Plain message
  - 'self | send_message text="<green>Done!</green>"'
  # With a placeholder
  - 'self | send_message text="<yellow>Welcome back, %player_name%!</yellow>"'
  # Richer MiniMessage: gradient plus hover and click
  - 'self | send_message text="<hover:show_text:''Click for details''><click:run_command:/menu><gradient:gold:yellow>Open menu</gradient></click></hover>"'

send_action_bar ​

Sends an action bar message. Target requirement REQUIRED_ENTITY

ParameterTypeRequiredDefaultDescription
textSTRINGYes—Action bar text in MiniMessage
yaml
actions:
  - 'self | send_action_bar text="<green>+50 XP</green>"'
  - 'self | send_action_bar text="<gray>Cooking: <green>████</green><dark_gray>██████</dark_gray> 40%</gray>"'

send_title ​

Shows a title and subtitle. Target requirement REQUIRED_ENTITY

ParameterTypeRequiredDefaultDescription
titleSTRINGYes—Main title in MiniMessage
subtitleSTRINGNo""Subtitle in MiniMessage
fade_inDURATIONNo10tFade-in time
stayDURATIONNo40tHold time
fade_outDURATIONNo10tFade-out time
yaml
actions:
  # Plain title
  - 'self | send_title title="<gold>Quest complete</gold>" subtitle="<gray>Experience awarded</gray>"'
  # Custom timings
  - 'self | send_title title="<red>Warning</red>" subtitle="<yellow>Entering a danger zone</yellow>" fade_in=5t stay=60t fade_out=20t'
  # Title only, quick flash
  - 'self | send_title title="<bold><gold>LEVEL UP!</gold></bold>" fade_in=0t stay=20t fade_out=5t'

broadcast_message ​

Broadcasts a MiniMessage message to the server. Target requirement NONE

ParameterTypeRequiredDefaultDescription
textSTRINGYes—Broadcast text in MiniMessage

No target is needed, so the source can be left out:

yaml
actions:
  # Server announcement
  - 'broadcast_message text="<gold>[Notice] <white>Restarting in 5 minutes</white></gold>"'
  # Achievement broadcast
  - 'broadcast_message text="<light_purple>✦ %player_name% completed a legendary forge! ✦</light_purple>"'

play_sound ​

Plays a sound for the target. Target requirement REQUIRED_ENTITY

ParameterTypeRequiredDefaultDescription
soundSOUNDYes—Sound key: a Bukkit Sound enum name or a minecraft: style key
volumeDOUBLENo1Volume; above 1 increases audible range
pitchDOUBLENo1Pitch, 0.5–2.0, where 1 is normal
yaml
actions:
  # Level-up chime
  - 'self | play_sound sound=minecraft:entity.player.levelup volume=1 pitch=1'
  # Anvil, lower pitch and audible from further away
  - 'self | play_sound sound=minecraft:block.anvil.use volume=2 pitch=0.8'
  # Play for everyone nearby
  - 'nearby_players radius=12 | play_sound sound=minecraft:entity.villager.no volume=1 pitch=1'

spawn_particle ​

Spawns particles at the target location. Target requirement REQUIRED_ANY

ParameterTypeRequiredDefaultDescription
particleSTRINGYes—Particle key (Bukkit Particle enum name)
countINTEGERNo1Particle count
offset_xDOUBLENo0Spread along X
offset_yDOUBLENo0Spread along Y
offset_zDOUBLENo0Spread along Z
extraDOUBLENo0Extra data (speed or data, depending on particle type)

Coordinates are no longer this stage's parameters: the source decides the location. self is the player's position, and at, offset, origin give an explicit one

yaml
actions:
  # Green particles on the player for success feedback
  - 'self | spawn_particle particle=happy_villager count=15 offset_x=0.3 offset_y=0.5 offset_z=0.3'
  # Flames on the player for a skill cast
  - 'self | spawn_particle particle=flame count=30 offset_x=0.5 offset_y=0.2 offset_z=0.5 extra=0.05'
  # Fixed world coordinates
  - 'at world=world x=100 y=65 z=-30 | spawn_particle particle=end_rod count=50 offset_x=1 offset_y=2 offset_z=1 extra=0.02'
  # Two blocks above the player
  - 'offset y=2 | spawn_particle particle=explosion count=3 offset_x=0.1 offset_y=0.1 offset_z=0.1'

boss_bar_show ​

Shows a boss bar to the target. Target requirement REQUIRED_ENTITY. Calling it again with the same id updates the existing bar

ParameterTypeRequiredDefaultDescription
idSTRINGYes—Boss bar id, used to update or hide it later
titleSTRINGYes—Bar title in MiniMessage
progressDOUBLENo1Progress, from 0 to 1
colorSTRINGNopurpleBar color
styleSTRINGNosolidBar style
flagsSTRINGNo""Comma-separated bar flags
yaml
actions:
  - 'self | boss_bar_show id=forge_progress title="<gold>Forging</gold>" progress=0.5 color=yellow'
  - 'self | boss_bar_show id=forge_progress title="<green>Almost done</green>" progress=0.9'

boss_bar_hide ​

Hides a boss bar shown earlier. Target requirement REQUIRED_ENTITY

ParameterTypeRequiredDefaultDescription
idSTRINGYes—Boss bar id, or all to hide every bar
yaml
actions:
  - 'self | boss_bar_hide id=forge_progress'
  - 'self | boss_bar_hide id=all'

reset_title ​

Clear the title and subtitle on screen. Category message, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

This stage has no dedicated parameters

yaml
actions:
  - 'self | reset_title'

send_title_fade ​

Send a title with fade-in and fade-out timings. Category message, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
titleSTRINGYesstring—Title text
subtitleSTRINGNostring``Subtitle text
fade_inINTEGERNointeger10Fade-in ticks
stayINTEGERNointeger40Ticks to stay on screen
fade_outINTEGERNointeger10Fade-out ticks
yaml
actions:
  - 'self | send_title_fade title=example'
  - 'self | send_title_fade title=example subtitle='

spawn_particle_at ​

Spawn particle effects at the given coordinates. Category visual, target NONE, execution domain LOCATION_REGION

ParameterTypeRequiredRangeDefaultDescription
particleSTRINGYesstring—Particle type
xDOUBLENofinite number0X offset
yDOUBLENofinite number0Y offset
zDOUBLENofinite number0Z offset
countINTEGERNointeger1Particle count
yaml
actions:
  - 'spawn_particle_at particle=flame'
  - 'spawn_particle_at particle=flame x=0'

Health and state ​

heal ​

Restores the target's health. Target requirement REQUIRED_ENTITY (it must be a living entity; non-living targets are skipped). Never exceeds max health

amount must be a finite, non-negative number; a negative or non-finite value fails the stage with INVALID_CONFIG instead of being treated as 0

IMPORTANT

This stage sets health directly and does not fire Bukkit's EntityRegainHealthEvent. No healing bonus or reduction applies

ParameterTypeRequiredDefaultDescription
amountDOUBLEYes—Health restored, in half-hearts (1 = half a heart)
yaml
actions:
  # Restore 4 points (two hearts)
  - 'self | heal amount=4'
  # Full heal; the excess is clamped to max health
  - 'self | heal amount=9999'
  # Small heal behind a roll
  - 'self | chance 30% | heal amount=2'

damage ​

Removes health from the target. Target requirement REQUIRED_ENTITY. Health floors at 0

amount must be a finite, non-negative number; a negative or non-finite value fails the stage with INVALID_CONFIG instead of being treated as 0

IMPORTANT

This stage sets health directly and does not fire Bukkit's EntityDamageEvent. Armor, resistance, and enchantments do not reduce it, and it cannot kill a player because the floor is 0

ParameterTypeRequiredDefaultDescription
amountDOUBLEYes—Damage amount
yaml
actions:
  # Remove 2 health (one heart)
  - 'self | damage amount=2'
  # Damage whatever is under the crosshair
  - 'looking_at range=10 | damage amount=8'
  # Behind a condition
  - 'if %player_world%=="world_nether" [ self | damage amount=1 ]'

set_health ​

Sets the target's health to a value. Target requirement REQUIRED_ENTITY. Clamped between 0 and max health

amount must be a finite, non-negative number; a negative or non-finite value fails the stage with INVALID_CONFIG

ParameterTypeRequiredDefaultDescription
amountDOUBLEYes—Target health

Use this stage with 0 to kill a player; kill_entity rejects player targets

yaml
actions:
  - 'self | set_health amount=20'
  - 'self | set_health amount=1'
  - 'looking_at | set_health amount=0'

feed ​

Restores the target's food level. Target requirement REQUIRED_ENTITY

ParameterTypeRequiredDefaultDescription
amountINTEGERNo20Food points restored
saturationDOUBLENo0Saturation restored

A negative amount fails the stage with INVALID_CONFIG; a negative saturation is treated as 0. Food and saturation never exceed 20

yaml
actions:
  - 'self | feed'
  - 'self | feed amount=6 saturation=3'

take_food ​

Removes food from the target, optionally draining saturation as well. Target requirement REQUIRED_ENTITY

ParameterTypeRequiredDefaultDescription
amountINTEGERYes—Food points to remove
saturationDOUBLENo0Extra saturation points to remove

amount must be greater than 0, otherwise the stage fails with INVALID_CONFIG. When the food level is insufficient it returns REJECTED (with the required and current values) and withdraws nothing; a negative saturation is treated as 0, and saturation floors at 0

yaml
actions:
  - 'self | take_food amount=6'
  # Drain two saturation points as well
  - 'self | take_food amount=4 saturation=2'
  # Insufficient food makes the stage reject instead of going negative
  - 'self | take_food amount=20'

ignite ​

Sets the target on fire. Target requirement REQUIRED_ENTITY

ParameterTypeRequiredDefaultDescription
durationDURATIONNo5sBurn duration
yaml
actions:
  - 'looking_at | ignite duration=8s'
  - 'nearby radius=5 limit=10 | ignite'

extinguish ​

Puts out the fire on the target. Target requirement REQUIRED_ENTITY. No parameters

yaml
actions:
  - 'self | extinguish'

kill_entity ​

Removes the target entity. Target requirement REQUIRED_ENTITY. No parameters

This is what remains of the old killentity once the search was taken out: radius, limit, type, and include_players all moved to the nearby source. Choosing which entities to act on was never this stage's job; now it simply removes whatever the flow hands it

Player targets are rejected. Calling Entity#remove on a player is not a supported operation — to kill a player, use set_health amount=0

yaml
actions:
  # Clear up to five zombies within eight blocks
  - 'nearby radius=8 limit=5 type=zombie | kill_entity'
  # Clear whatever is under the crosshair
  - 'looking_at range=20 | kill_entity'

projectile ​

Launches a self-driven projectile from the caster. Target requirement OPTIONAL

ParameterTypeRequiredDefaultDescription
speedDOUBLENo1.5Blocks travelled per tick
gravityDOUBLENo0.05Downward pull per tick
lifetimeINTEGERNo60Maximum lifetime in ticks
hit_radiusDOUBLENo0.5Hit detection radius
pierceINTEGERNo0How many extra entities it passes through
homingBOOLEANNofalseSteer toward the current target
homing_strengthDOUBLENo0.1Homing turn strength
particleSTRINGNoflameTrail particle key
damageDOUBLENo0Damage dealt on hit; 0 means none
directionSTRINGNolookInitial direction: look (line of sight) or target
yaml
actions:
  # Straight fireball
  - 'self | projectile speed=2 damage=8 particle=flame'
  # Homing shot, which needs a target first
  - 'looking_at range=25 | projectile homing=true homing_strength=0.2 damage=12 direction=target'
  # Piercing shot
  - 'self | projectile speed=2.5 pierce=3 damage=5 gravity=0'

set_absorption ​

Set the entity's absorption health. Category entity, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
amountDOUBLEYesfinite number—Absorption health
yaml
actions:
  - 'self | set_absorption amount=1.0'

set_arrows_in_body ​

Set the number of arrows stuck in the entity. Category entity, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
countINTEGERYesinteger—Number of arrows
yaml
actions:
  - 'self | set_arrows_in_body count=3'

set_collidable ​

Toggle collisions between mobs. Category entity, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
collidableBOOLEANYestrue / false—Whether collisions apply
yaml
actions:
  - 'self | set_collidable collidable=true'

set_max_no_damage_ticks ​

Set the entity's damage-immunity cap in ticks. Category entity, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
ticksINTEGERYesinteger—Damage-immunity cap
yaml
actions:
  - 'self | set_max_no_damage_ticks ticks=3'

set_no_damage_ticks ​

Set the entity's damage-immunity duration in ticks. Category entity, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
ticksINTEGERYesinteger—Damage-immunity duration in ticks
yaml
actions:
  - 'self | set_no_damage_ticks ticks=3'

set_remaining_air ​

Set the entity's remaining air ticks. Category entity, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
ticksINTEGERYesinteger—Remaining air in ticks
yaml
actions:
  - 'self | set_remaining_air ticks=200'

Entity metadata ​

add_scoreboard_tag ​

Add a scoreboard tag to the entity. Category meta, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
tagSTRINGYesstring—Tag text
yaml
actions:
  - 'self | add_scoreboard_tag tag=example'

remove_scoreboard_tag ​

Remove a scoreboard tag from the entity. Category meta, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
tagSTRINGYesstring—Tag text
yaml
actions:
  - 'self | remove_scoreboard_tag tag=example'

set_ai ​

Toggle the mob's AI. Category meta, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
enabledBOOLEANYestrue / false—Whether AI is enabled
yaml
actions:
  - 'self | set_ai enabled=true'

set_can_pickup_items ​

Toggle whether the mob picks up dropped items. Category meta, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
can_pickupBOOLEANYestrue / false—Whether items can be picked up
yaml
actions:
  - 'self | set_can_pickup_items can_pickup=true'

set_fire_ticks ​

Set the burning duration in ticks (negative extinguishes). Category meta, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
ticksINTEGERYesinteger—Burning duration in ticks
yaml
actions:
  - 'self | set_fire_ticks ticks=100'

set_glowing ​

Toggle the glowing outline. Category meta, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
glowingBOOLEANYestrue / false—Whether the entity glows
yaml
actions:
  - 'self | set_glowing glowing=true'

set_gravity ​

Toggle gravity for the entity. Category meta, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
gravityBOOLEANYestrue / false—Whether gravity applies
yaml
actions:
  - 'self | set_gravity gravity=true'

set_invulnerable ​

Toggle invulnerability. Category meta, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
invulnerableBOOLEANYestrue / false—Whether the entity is invulnerable
yaml
actions:
  - 'self | set_invulnerable invulnerable=true'

set_name ​

Set the entity's custom name (MiniMessage). Category meta, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
nameSTRINGYesstring—Name text
yaml
actions:
  - 'self | set_name name=<gold>Boss</gold>'

set_name_visible ​

Toggle whether the custom name is always visible. Category meta, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
visibleBOOLEANYestrue / false—Whether the name is always visible
yaml
actions:
  - 'self | set_name_visible visible=true'

set_persistent ​

Toggle whether the entity survives a chunk unload. Category meta, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
persistentBOOLEANYestrue / false—Whether to keep the entity
yaml
actions:
  - 'self | set_persistent persistent=true'

set_remove_when_far_away ​

Toggle removal when too far from players. Category meta, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
removeBOOLEANYestrue / false—Whether the mob is removed
yaml
actions:
  - 'self | set_remove_when_far_away remove=true'

set_silent ​

Toggle entity silence. Category meta, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
silentBOOLEANYestrue / false—Whether the entity is silent
yaml
actions:
  - 'self | set_silent silent=true'

set_visible_by_default ​

Toggle default visibility to all players. Category meta, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
visibleBOOLEANYestrue / false—Whether it is visible to all players by default
yaml
actions:
  - 'self | set_visible_by_default visible=true'

Movement and velocity ​

add_velocity ​

Add a velocity vector to the entity (knockback/launch). Category movement, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
xDOUBLEYesfinite number—Velocity delta along X
yDOUBLEYesfinite number—Velocity delta along Y
zDOUBLEYesfinite number—Velocity delta along Z
yaml
actions:
  - 'self | add_velocity x=1.0 y=1.0 z=1.0'

knockback ​

Push the target away from the caster. Category movement, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
strengthDOUBLENofinite number1Knockback strength
verticalDOUBLENofinite number0.4Upward velocity
yaml
actions:
  - 'self | knockback'
  - 'self | knockback strength=1'

set_rotation ​

Set the entity's facing (yaw/pitch). Category movement, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
yawDOUBLEYesfinite number—Horizontal yaw angle
pitchDOUBLEYesfinite number—Pitch angle
yaml
actions:
  - 'self | set_rotation yaw=1.0 pitch=1.0'

set_velocity ​

Set the entity's velocity vector. Category movement, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
xDOUBLEYesfinite number—Velocity along X
yDOUBLEYesfinite number—Velocity along Y
zDOUBLEYesfinite number—Velocity along Z
yaml
actions:
  - 'self | set_velocity x=1.0 y=1.0 z=1.0'

Potion effects ​

give_potion_effect ​

Adds a potion effect to the target. Target requirement REQUIRED_ENTITY

ParameterTypeRequiredDefaultDescription
typeSTRINGYes—Effect type, written as speed, minecraft:strength, and so on
levelINTEGERYes—Effect level, one-based. 1 maps to Bukkit amplifier 0 (in-game I)
durationDURATIONYes—Duration, such as 100t, 5s, 10000ms
ambientBOOLEANNofalseAmbient effect (sparser, more transparent particles)
particlesBOOLEANNotrueShow particles
iconBOOLEANNotrueShow the effect icon in the HUD
yaml
actions:
  # Speed II for 30 seconds
  - 'self | give_potion_effect type=speed level=2 duration=30s particles=true icon=true'
  # Strength I for a minute, no particles for a subtler look
  - 'self | give_potion_effect type=strength level=1 duration=60s particles=false icon=true'
  # Night vision as an ambient effect
  - 'self | give_potion_effect type=night_vision level=1 duration=5s ambient=true'
  # Namespaced form
  - 'self | give_potion_effect type=minecraft:regeneration level=3 duration=10s'
  # Slow everything nearby
  - 'nearby radius=6 limit=10 | give_potion_effect type=slowness level=2 duration=5s'

remove_potion_effect ​

Removes one potion effect from the target. Target requirement REQUIRED_ENTITY

ParameterTypeRequiredDefaultDescription
typeSTRINGYes—Effect type
yaml
actions:
  - 'self | remove_potion_effect type=slowness'
  - 'self | remove_potion_effect type=minecraft:weakness'

clear_potion_effects ​

Clears every active potion effect on the target. Target requirement REQUIRED_ENTITY. No parameters

yaml
actions:
  # Cleanse
  - 'self | clear_potion_effects'
  # Cleanse two seconds later
  - 'self | after 2s | clear_potion_effects'
  # Only in a specific world
  - 'if %player_world%=="world_arena" [ self | clear_potion_effects ]'

Items ​

Several item stages share the same item_source parameter, whose value is the CoreLib hyphenated item source shorthand. See Item Sources

The slot parameter accepts:

  • mainhand, main_hand, hand
  • offhand, off_hand
  • helmet, chestplate/chest, leggings/legs, boots
  • inventory indexes 0 to 35, also writable as slot_0 or hotbar_0

Item-amount stages (give_item, set_item, take_item, drop_item) require an amount greater than 0, otherwise they fail with INVALID_CONFIG. take_item returns REJECTED (with the required and current values) when the inventory holds too few, and withdraws nothing. repair_item and damage_item treat amount differently — see their sections

send_item ​

Hands the pipeline item to the target. Target requirement REQUIRED_ENTITY. No parameters

This stage declares that it needs the pipeline item, which makes a send_item without a preceding create_item a load-time error rather than a runtime null: the validator compares that declaration against the context the trigger phase promises to provide

The old id and keep parameters are both gone. The pipeline holds a single item key rather than a table indexed by name, and reading a context value does not consume it, so there is no longer anything to "keep in temporary storage"

yaml
actions:
  - 'self | create_item item_source=minecraft-golden_apple amount=2 | send_item'

give_item ​

Gives the item for an item source into the target's inventory. Target requirement REQUIRED_ENTITY

ParameterTypeRequiredDefaultDescription
item_sourceSTRINGNo""Item source
amountINTEGERNo1Amount

The difference from create_item plus send_item: this stage gives straight from the source without going through the pipeline item, so it needs only one stage, but it also gives you no chance to do anything to the item first

yaml
actions:
  - 'self | give_item item_source=minecraft-diamond amount=5'
  - 'nearby_players radius=10 | give_item item_source=emakiitem-strengthen_stone amount=1'

set_item ​

Sets the item in one of the target's slots from an item source. Target requirement REQUIRED_ENTITY

ParameterTypeRequiredDefaultDescription
slotSTRINGNomainhandInventory slot
item_sourceSTRINGNo""Item source
amountINTEGERNo1Amount
yaml
actions:
  - 'self | set_item slot=mainhand item_source=emakiitem-flame_sword'
  - 'self | set_item slot=helmet item_source=minecraft-diamond_helmet'

clear_item ​

Clears the item in one of the target's slots. Target requirement REQUIRED_ENTITY

ParameterTypeRequiredDefaultDescription
slotSTRINGYes—Inventory slot
item_sourceSTRINGNo""Expected item source. When set, the slot is only cleared on a match, otherwise skipped
yaml
actions:
  # Clear the main hand unconditionally
  - 'self | clear_item slot=mainhand'
  # Only clear it if it holds a stick
  - 'self | clear_item slot=mainhand item_source=minecraft-stick'
  # Clear the helmet slot only for a matching custom item
  - 'self | clear_item slot=helmet item_source=emakiitem-cursed_helmet'
  # Clear a specific inventory index
  - 'self | clear_item slot=0'

take_item ​

Removes matching items from the target's inventory. Target requirement REQUIRED_ENTITY

ParameterTypeRequiredDefaultDescription
item_sourceSTRINGNo""Item source to remove
amountINTEGERNo1Amount to remove
yaml
actions:
  - 'self | take_item item_source=minecraft-emerald amount=3'
  - 'self | take_item item_source=emakiitem-forge_token amount=1'

drop_item ​

Drops an item at the target location. Target requirement REQUIRED_ANY

ParameterTypeRequiredDefaultDescription
item_sourceSTRINGNo""Item source
amountINTEGERNo1Item amount

The coordinates come from the source and are no longer this stage's parameters

yaml
actions:
  # Drop above the player
  - 'offset y=2 | drop_item item_source=minecraft-diamond amount=1'
  # Drop a reward at fixed coordinates
  - 'at world=world x=100 y=65 z=-50 | drop_item item_source=minecraft-gold_ingot amount=5'
  # Drop three blocks in front of the player
  - 'offset z=3 relative=true | drop_item item_source=minecraft-emerald amount=2'

repair_item ​

Repairs the durability of the item in a slot. Target requirement REQUIRED_ENTITY

ParameterTypeRequiredDefaultDescription
slotSTRINGNomainhandInventory slot
amountINTEGERNo0Durability points repaired; 0 or less repairs fully
yaml
actions:
  - 'self | repair_item'
  - 'self | repair_item slot=chestplate amount=50'

damage_item ​

Adds durability damage to the item in a slot. Target requirement REQUIRED_ENTITY

ParameterTypeRequiredDefaultDescription
slotSTRINGNomainhandInventory slot
amountINTEGERNo1Durability damage added
delete_itemBOOLEANNofalseRemove the item when durability runs out

A negative amount returns INVALID_CONFIG; an amount of 0 skips the stage without touching the item

yaml
actions:
  - 'self | damage_item amount=10'
  - 'self | damage_item slot=mainhand amount=100 delete_item=true'

Player state ​

clear_inventory ​

Clear the player's inventory. Category player, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

This stage has no dedicated parameters

yaml
actions:
  - 'self | clear_inventory'

set_bed_spawn ​

Set the player's respawn point to the origin. Category player, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

This stage has no dedicated parameters

yaml
actions:
  - 'self | set_bed_spawn'

set_compass_target ​

Point the player's compass at the origin. Category player, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

This stage has no dedicated parameters

yaml
actions:
  - 'self | set_compass_target'

set_exp_to_level ​

Set the player's XP bar percentage directly. Category player, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
percentINTEGERYes0 to 100—XP bar percentage, 0-100
yaml
actions:
  - 'self | set_exp_to_level percent=3'

set_fly_speed ​

Set the player's flight speed. Category player, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
speedDOUBLEYes-1.0 to 1.0—Flight speed, -1.0 to 1.0
yaml
actions:
  - 'self | set_fly_speed speed=1.0'

set_gamemode ​

Set the player's game mode. Category player, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
gamemodeSTRINGYessurvival / creative / adventure / spectator—survival / creative / adventure / spectator
yaml
actions:
  - 'self | set_gamemode gamemode=creative'

set_item_cooldown ​

Set a cooldown for the given material on the player. Category player, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
materialMATERIALYesBukkit Material name—Item material
secondsINTEGERYesinteger—Cooldown in seconds
yaml
actions:
  - 'self | set_item_cooldown material=golden_apple seconds=60'

set_level ​

Set the player's experience level directly. Category player, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
levelINTEGERYesinteger—Experience level
yaml
actions:
  - 'self | set_level level=3'

set_saturation ​

Set the player's saturation. Category player, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
valueDOUBLEYesfinite number—Saturation value
yaml
actions:
  - 'self | set_saturation value=1.0'

set_walk_speed ​

Set the player's walk speed. Category player, target REQUIRED_ENTITY, execution domain CONTEXT_ENTITY

ParameterTypeRequiredRangeDefaultDescription
speedDOUBLEYes-1.0 to 1.0—Walk speed, -1.0 to 1.0
yaml
actions:
  - 'self | set_walk_speed speed=1.0'

Blocks and world ​

place_block ​

Places the block for an item source at the target location. Target requirement REQUIRED_ANY

ParameterTypeRequiredDefaultDescription
item_sourceSTRINGNo""Block item source

Vanilla blocks work, as do CraftEngine, ItemsAdder, Nexo, and Oraxen custom blocks. If the source is not a placeable block, the stage skips rather than breaking the pipeline. With a player context it fires the Bukkit place event, so a protection plugin cancelling it means nothing is placed

yaml
actions:
  # Vanilla block; note the hyphen between namespace and id
  - 'at world=world x=100 y=64 z=-20 | place_block item_source=minecraft-stone'
  # Under the player's feet
  - 'at x=~ y=~-1 z=~ | place_block item_source=minecraft-oak_planks'
  # CraftEngine custom block
  - 'at x=~1 y=~ z=~ | place_block item_source=ce-cutting_board'
  # ItemsAdder custom block
  - 'offset y=2 | place_block item_source=itemsadder-decorative_lamp'

set_block ​

Sets the block at the target location. Target requirement REQUIRED_ANY

ParameterTypeRequiredDefaultDescription
materialSTRINGNo""Block material name
block_dataSTRINGNo""Bukkit BlockData string
apply_physicsBOOLEANNotrueTrigger a block physics update

The difference from place_block: this stage takes a material directly instead of an item source and does not fire the Bukkit place event, so it can set block states that have no corresponding item

yaml
actions:
  - 'at world=world x=50 y=70 z=100 | set_block material=chest'
  - 'at x=~ y=~-1 z=~ | set_block material=oak_stairs block_data="[facing=north,half=top]"'
  - 'origin | set_block material=air apply_physics=false'

break_block ​

Breaks or clears the block at the target location. Target requirement REQUIRED_ANY

ParameterTypeRequiredDefaultDescription
drop_itemsBOOLEANNofalseDrop the block's items
apply_physicsBOOLEANNotrueTrigger a physics update when clearing the block
yaml
actions:
  - 'at x=~ y=~-1 z=~ | break_block drop_items=true'
  - 'at world=world x=10 y=64 z=10 | break_block'

explosion ​

Creates an explosion at the target location. Target requirement REQUIRED_ANY

ParameterTypeRequiredDefaultDescription
powerDOUBLENo0Explosion power
fireBOOLEANNofalseSet fire
break_blocksBOOLEANNofalseBreak blocks

The defaults — power 0, no fire, no block damage — give you sound and visuals only. Real destruction has to be turned on explicitly

yaml
actions:
  # Cosmetic explosion
  - 'self | explosion power=2'
  # Explosion that actually breaks blocks
  - 'looking_at range=15 | explosion power=4 fire=true break_blocks=true'

spawn_entity ​

Spawns entities at the target location. Target requirement REQUIRED_ANY

ParameterTypeRequiredDefaultDescription
typeENTITY_TYPEYes—Entity type
countINTEGERNo1How many to spawn
yaml
actions:
  - 'at world=world x=100 y=65 z=-30 | spawn_entity type=zombie count=3'
  - 'offset z=4 relative=true | spawn_entity type=armor_stand'

set_difficulty ​

Set the world difficulty. Category world, target NONE, execution domain SERVER_GLOBAL

ParameterTypeRequiredRangeDefaultDescription
difficultySTRINGYespeaceful / easy / normal / hard—Difficulty: peaceful / easy / normal / hard
yaml
actions:
  - 'set_difficulty difficulty=normal'

set_game_rule ​

Set a vanilla game rule of the world. Category world, target NONE, execution domain SERVER_GLOBAL

ParameterTypeRequiredRangeDefaultDescription
ruleSTRINGYesstring—Game rule name
valueSTRINGYestrue / false or an integer, per rule type—Rule value
yaml
actions:
  - 'set_game_rule rule=doDaylightCycle value=false'

set_weather ​

Set the world weather. Category world, target NONE, execution domain SERVER_GLOBAL

ParameterTypeRequiredRangeDefaultDescription
weatherSTRINGYesclear / rain / thunder—Weather: clear / rain / thunder
durationINTEGERNointeger0Duration in seconds; 0 means unlimited
yaml
actions:
  - 'set_weather weather=rain'
  - 'set_weather weather=rain duration=0'

set_world_spawn ​

Set the world spawn point to the origin. Category world, target NONE, execution domain LOCATION_REGION

This stage has no dedicated parameters

yaml
actions:
  - 'set_world_spawn'

set_world_time ​

Set the world time. Category world, target NONE, execution domain SERVER_GLOBAL

ParameterTypeRequiredRangeDefaultDescription
timeSTRINGYestick count or day / night—Time as tick count or day/night
yaml
actions:
  - 'set_world_time time=day'

Teleport ​

teleport ​

Teleports the target to a coordinate. Target requirement REQUIRED_ENTITY

ParameterTypeRequiredDefaultDescription
worldSTRINGNo""Destination world; empty uses the target's current world
xSTRINGNo~X coordinate. Supports ~ relative notation (~5 means X+5)
ySTRINGNo~Y coordinate. Supports ~
zSTRINGNo~Z coordinate. Supports ~
yawDOUBLENotarget's currentYaw (horizontal facing, 0–360). Omitted keeps the current facing
pitchDOUBLENotarget's currentPitch (vertical facing, -90–90). Omitted keeps the current facing

The coordinates are this stage's own parameters rather than something the source provides: the source decides who gets teleported, the coordinates decide where to, and both are necessary

yaml
actions:
  # Fixed coordinates
  - 'self | teleport world=world x=0 y=100 z=0 yaw=180 pitch=0'
  # Relative, ten blocks up
  - 'self | teleport y=~10'
  # Across worlds
  - 'self | teleport world=world_nether x=50 y=64 z=-100 yaw=90 pitch=0'
  # Pull every nearby player to spawn
  - 'nearby_players radius=30 | teleport world=world x=0 y=65 z=0'

Economy ​

The three economy stages share one parameter set

ParameterTypeRequiredDefaultDescription
amountDOUBLEYes—Amount
providerSTRINGNoautoEconomy provider: auto, vault, or excellenteconomy
currencySTRINGNo""Currency id. Required when ExcellentEconomy runs multiple currencies

amount must be a finite, non-negative number; NaN, Infinity, and negative values all return INVALID_CONFIG. give_money and take_money additionally require an amount greater than 0, while set_money allows 0 (used to clear a balance). An unavailable provider or an unknown currency is also treated as INVALID_CONFIG

give_money ​

Adds to the target's balance. Target requirement REQUIRED_ENTITY

yaml
actions:
  # Basic, letting CoreLib pick the economy plugin
  - 'self | give_money amount=100'
  # Explicitly Vault
  - 'self | give_money amount=500 provider=vault'
  # ExcellentEconomy with multiple currencies
  - 'self | give_money amount=50 provider=excellenteconomy currency=gems'

take_money ​

Takes from the target's balance. Target requirement REQUIRED_ENTITY. An insufficient balance returns REJECTED (with the required and current values); the balance never goes negative and nothing is partially withdrawn

yaml
actions:
  - 'self | take_money amount=25.5 provider=vault'
  - 'self | take_money amount=10 provider=excellenteconomy currency=coins'

set_money ​

Sets the target's balance to a value. Target requirement REQUIRED_ENTITY

yaml
actions:
  - 'self | set_money amount=0 provider=vault'
  - 'self | set_money amount=1000 provider=auto'

Experience ​

The three experience stages share one parameter set, and all require REQUIRED_ENTITY

ParameterTypeRequiredDefaultDescription
amountINTEGERYes—Experience points or levels
modeSTRINGNopointsMode: points (experience points) or levels

give_exp and set_exp require a non-negative amount; a negative value returns INVALID_CONFIG. take_exp requires an amount greater than 0

give_exp ​

Adds experience to the target

yaml
actions:
  # 100 experience points
  - 'self | give_exp amount=100 mode=points'
  # Three levels
  - 'self | give_exp amount=3 mode=levels'
  # points is the default and can be omitted
  - 'self | give_exp amount=50'

take_exp ​

Removes experience from the target. When the target has too little, it returns REJECTED (with the mode plus the required and current values) and withdraws nothing; experience never goes negative. points mode measures total experience, levels mode measures levels

yaml
actions:
  - 'self | take_exp amount=200 mode=points'
  - 'self | take_exp amount=1 mode=levels'

set_exp ​

Sets the target's total experience or level

yaml
actions:
  - 'self | set_exp amount=30 mode=levels'
  - 'self | set_exp amount=0 mode=points'

Commands ​

The three command stages share one parameter. A leading / is stripped automatically, but leaving it out of config reads better

ParameterTypeRequiredDefaultDescription
commandSTRINGYes—The command to run, without a leading /

run_command_as_player ​

Runs a command as the target player. Target requirement REQUIRED_ENTITY. Subject to the player's own permissions

yaml
actions:
  - 'self | run_command_as_player command="spawn"'
  - 'self | run_command_as_player command="menu open main"'

run_command_as_op ​

Temporarily grants the target player OP, runs the command, then restores the original OP state. Target requirement REQUIRED_ENTITY

CAUTION

This stage grants OP temporarily, which carries real risk: for the duration of the command the player holds full OP. If any part of the command comes from a placeholder the player can influence, you have handed out a privilege escalation path. Prefer run_command_as_console, or grant a temporary permission through your permissions plugin

yaml
actions:
  - 'self | run_command_as_op command="lp user %player_name% permission set example.vip true"'

run_command_as_console ​

Runs a command as the console. Target requirement NONE (placeholders are still resolved)

No target is needed, so the source can be left out:

yaml
actions:
  # Server announcement
  - 'run_command_as_console command="say %player_name% completed a forge"'
  # Grant a permission
  - 'run_command_as_console command="lp user %player_name% permission set forge.master true"'
  # Call another plugin
  - 'run_command_as_console command="crate give %player_name% legendary 1"'

Long-running tasks ​

every covers short bursts inside a single pipeline. For a loop that runs for a long time, can be cancelled by key, and stops on its own when the player logs off or dies, use the two stages below

start_task ​

Starts a named sequence that repeats on an interval. Target requirement OPTIONAL

ParameterTypeRequiredDefaultDescription
sequenceSTRINGYes—Name of the sequence to repeat
timesINTEGERNo1How many times to run
intervalDURATIONNo20tInterval between runs
initial_delayDURATIONNo0tDelay before the first run
keySTRINGNo""Task key, used to cancel it later
on_conflictSTRINGNoreplaceKey conflict policy: replace, ignore, allow_duplicate
stop_when_offlineBOOLEANNotrueStop when the player logs off
stop_when_deadBOOLEANNofalseStop when the player dies
stop_whenSTRINGNo""Stop once this condition holds
stop_on_failureBOOLEANNofalseStop when the sequence fails

This one stage replaces the old loopsync and loopasync. In the old implementation the async marker only picked a different minimum interval and ran one extra pre-check; the scheduling was identical either way. Real thread affinity now comes from the execution domain each stage declares for itself, so there is nothing left for a config-level sync/async switch to express

Any parameter not in the table above is passed down as a sequence argument and read inside the sequence as %var.<name>%. This replaces the old with. prefix, which only existed because the old parameter model could not tell declared parameters from extra ones. It can now

yaml
action:
  templates:
    burn_tick:
      - 'inherited | damage amount=1'
      - 'inherited | spawn_particle particle=flame count=5'
    buff_pulse:
      - 'self | send_action_bar text="<gold>Blessing: %var.label%</gold>"'

actions:
  # Once a second, ten times
  - 'looking_at | keep | start_task sequence=burn_tick times=10 interval=20t key=burn_%player_name%'
  # With a sequence argument, stopping on death
  - 'self | start_task sequence=buff_pulse times=30 interval=1s key=buff stop_when_dead=true label=Holy_Blessing'
  # Stop as soon as the condition no longer holds
  - 'self | start_task sequence=buff_pulse times=100 interval=20t stop_when=%player_world%!="world_arena"'

stop_task ​

Cancels running tasks by key. Target requirement NONE

ParameterTypeRequiredDefaultDescription
keySTRINGYes—Task key to cancel
matchSTRINGNoexactMatch mode: exact or prefix

Cancelling nothing counts as a skip, not a failure. Config routinely cancels a task that may not be running — cleaning up a buff loop on an event that fires in both cases, for instance — and treating that as an error would fill the log with correct configuration

yaml
actions:
  - 'stop_task key=burn_%player_name%'
  - 'stop_task key=buff_ match=prefix'

Scripts ​

The three js_* stages run GraalJS scripts. They share the same parameters and differ only in target requirement and execution domain, so picking the wrong one means the script touches Bukkit API from the wrong thread

ParameterTypeRequiredDefaultDescription
codeSTRINGYes—JavaScript source
timeoutINTEGERNo5000Timeout in milliseconds

A timeout of zero or less falls back to the default. Empty code, a timeout, and an interruption all count as skipped rather than failed; only a thrown script error fails the stage

The script's evaluated value is written to a single key, script_result, in that stage's own outcome data. It does not become a pipeline variable, so later stages cannot read %var.script_result%

js_compute ​

Pure computation with no Bukkit state access. Target requirement NONE, execution domain ASYNC_COMPUTE. Binds context, plus player when the current target is a player

Because it runs on an async thread, the script must not touch Bukkit state

Inside the script, context.getVariable(name) and context.hasVariable(name) read pipeline variables

yaml
actions:
  - 'self | set level=%player_level% | js_compute code="context.getVariable(''level'') * 10"'

js_entity ​

Scripts that act on a player or entity. Target requirement REQUIRED_ENTITY, execution domain CONTEXT_ENTITY. Binds player and context; skipped when the target is not a player

js_location ​

Scripts that act on a block or location. Target requirement REQUIRED_LOCATION, execution domain LOCATION_REGION. Binds location and context, plus a read-only player when the current target is a player

MythicMobs and skills ​

cast_mythic_skill and cast_skill come from the EmakiSkills module and are not CoreLib built-ins. For their parameters and behaviour, see Skills CoreLib Actions

Listing and running pipelines from commands ​

Administrators can inspect the current registry and run a pipeline by hand from inside the server:

text
/corelib action list
/corelib action run <pipeline text>

The same subcommand is available through /emakicorelib action, /emakicore action, and the plural alias actions. It reuses the emakicorelib.admin permission. Console execution has no player context, so stages needing an entity target skip through the normal target-requirement path. The run keyword is optional: /corelib action self | heal amount=4 is equivalent to spelling it out

action list groups ids by role (source / gate / action) and shows the owning plugin. Grouping rather than a flat list is deliberate: an id is only legal in one position within a pipeline, and a flat list cannot tell an operator where it may be used

action run compiles before it executes, so a syntax error or unknown stage name arrives with its own diagnostic instead of a vague execution failure. Only the first diagnostic is shown — the compiler reports every problem on a line, and the rest are usually consequences of the first. When execution finishes it reports the pipeline's overall result, naming the failing stage on failure, because a hand-typed pipeline is normally being debugged and the failing stage is the answer

Where to find sub-plugin stages ​

CoreLib owns the stage registry and the pipeline engine. Business plugins may append their own sources, gates, and actions to that same registry when they enable. To avoid a stale catalogue of every plugin's stage parameters, only CoreLib built-ins are documented here

Stages registered by sub-plugins live on those plugins' own action pages:

Source moduleDocumentation
EmakiAttributeAttribute CoreLib Actions
EmakiForgeForge CoreLib Actions
EmakiStrengthenStrengthen CoreLib Actions
EmakiCookingCooking CoreLib Actions
EmakiGemGem CoreLib Actions
EmakiLevelLevel CoreLib Actions
EmakiSkillsSkills CoreLib Actions
EmakiItemItem CoreLib Actions
EmakiCodexCodex CoreLib Actions
EmakiStorageStorage CoreLib Actions

The 4 sources and 4 actions registered by EmakiMobs have no dedicated actions page yet; they are listed by ID and purpose on the Stage ID table only

When a stage comes from a business plugin, its parameters, context variables, and execution timing are defined by that plugin's documentation

Registering stages from third-party plugins ​

External developers can depend on emaki-corelib-api alone and register custom stages into CoreLib's shared registry through the method matching the role:

  • EmakiCoreLibApi.registerActionStage(plugin, stage) — register an action
  • EmakiCoreLibApi.registerActionSource(plugin, source) — register a source
  • EmakiCoreLibApi.registerActionGate(plugin, gate) — register a gate

CoreLib automatically revokes stages and rebuild callbacks when the owner plugin is disabled; callers should still close returned handles during normal lifecycle cleanup

An action implements CoreActionStage and declares id, category, description, parameters, targetRequirement, requiredContext, and executionTarget. CoreLib reuses the same lexer, grammar, parameter validation, placeholder rendering, scheduling, and debug output

executionTarget has no default implementation, and that is intentional: a stage has to state which thread it runs on rather than inheriting a vague default. requiredContext is the mechanism that turns "missing an upstream stage" into a load-time error — send_item declares that it needs the pipeline item, so the validator catches a missing create_item while the config is loading

Minimal example:

java
CoreStageRegistration registration = EmakiCoreLibApi.registerActionStage(plugin, new CoreActionStage() {
    @Override
    public String id() {
        return "my_custom_stage";
    }

    @Override
    public String category() {
        return "myplugin";
    }

    @Override
    public String description() {
        return "Runs my plugin's custom effect.";
    }

    @Override
    public List<CoreStageParameter> parameters() {
        return List.of(CoreStageParameter.required("value", CoreStageParameterType.STRING, "Custom value"));
    }

    @Override
    public CoreTargetRequirement targetRequirement() {
        return CoreTargetRequirement.REQUIRED_ENTITY;
    }

    @Override
    public CoreActionExecutionTarget executionTarget(CoreStagePlanningContext context) {
        // Pick the entity's own thread when you need main-thread Bukkit APIs
        return CoreActionExecutionTarget.contextEntity();
    }

    @Override
    public CoreActionOutcome execute(CoreStageContext context, CoreResolvedArguments arguments) {
        // Your plugin logic goes here
        return CoreActionOutcome.success();
    }
});

// On plugin disable:
registration.close();

Stage ID reference ​

To look up all 218 stages (131 built into CoreLib — 25 source / 16 gate / 90 action — plus 87 registered by this repository's plugins) grouped with purpose text, registering plugin and jump targets, use the Stage ID table page. Per-stage dedicated parameters for built-in stages are the tables in each section below, stages registered by a plugin follow that plugin's own actions page as listed under Where to find sub-plugin stages, and the control decorators shared by all three roles are covered under Common parameters

Troubleshooting ​

Unknown stage name at load time. Check that the id is underscore-separated: it is send_message, not sendmessage. Confirm the id exists with /corelib action list, and confirm it appears in the right position — a source cannot sit where an action belongs

Right stage name, but "unknown parameter". A few parameter names changed too. Item sources are uniformly item_source (the source and item aliases are gone), set_block has no block alias, and extra start_task arguments are written directly without a with. prefix

The pipeline was skipped without an error. An unmet target requirement skips rather than fails. The usual cause is a missing source that fell back to self, where self is empty without a player context — or a nearby that found nothing

|| is not working in a branch condition. Logical OR must be written ||. A single | is read as a stage separator

send_item errors at load time. The stage needs the pipeline item, so the same pipeline must have a create_item ahead of it

times is reported as out of range. every ... times N is bounded by action.pipeline.max_repeat_times (default 100). For more repetitions, use start_task, whose count quota is governed separately by the task service

Spaces in a value got split apart. Values containing spaces, MiniMessage tags, or commands must be quoted. Wrapping the whole pipeline line in single quotes in YAML is also worth doing, so : and # are not interpreted by YAML

A complete example ​

A full post-forge success config, showing several stage types working together:

yaml
action:
  success:
    # Core feedback
    - 'self | send_title title="<gold>Forge complete</gold>" subtitle="<gray>Quality: %forge_quality%</gray>" fade_in=5t stay=40t fade_out=10t'
    - 'self | play_sound sound=minecraft:block.anvil.use volume=1 pitch=1.2'
    - 'self | spawn_particle particle=happy_villager count=20 offset_x=0.5 offset_y=0.5 offset_z=0.5'

    # Economy reward: a 10% chance to refund part of the cost
    - 'self | chance 10% | give_money amount=50 provider=vault | send_message text="<yellow>Lucky! 50 coins refunded.</yellow>"'

    # Experience reward
    - 'self | give_exp amount=30 mode=points'

    # Extra reward for high quality
    - 'if %forge_quality%=="epic" [ self | send_message text="<light_purple>Epic quality! Bonus strengthen stone.</light_purple>" | create_item item_source=emakiitem-strengthen_stone amount=1 | send_item ]'

    # Server broadcast, legendary only
    - 'if %forge_quality%=="legendary" [ broadcast_message text="<gold>%player_name% forged legendary gear!</gold>" ]'

    # Delayed reminder
    - 'self | after 2s | send_message text="<gray>Your gear is updated; check your inventory.</gray>"'

Note the second reward line: once chance passes, both give_money and send_message further along the same pipeline run. The old syntax needed the chance written on both lines, rolling twice independently, which could pay out the money without showing the message. One pipeline rolls once, and every stage after it shares that result