Events and scheduling
JavaScript exposes events through world.beforeEvents, world.afterEvents,
system.beforeEvents, and system.afterEvents; Python uses
world.before_events, world.after_events, system.before_events, and
system.after_events. Subscribe with subscribe and remove the same handler
with unsubscribe.
Before and After
Before Events run synchronously during the native Minecraft event. A cancelable event contains cancel. Keep these
handlers small and schedule deferred work through system.
After Events report an outcome that has already happened and cannot cancel it. QuestScript captures bounded event input and delivers handlers on the server thread during the tick's final scripting phase. Events are grouped by type; occurrences and subscribers inside a group preserve enqueue and registration order.
The playerJoin snapshot contains stable playerId and playerName fields like Bedrock, but no live player object. If
the player is still connected, resolve it separately through world.getPlayer(event.playerId) /
world.get_player(event.player_id). Asynchronous playerBreakBlock also captures playerId and playerName; its
player field is nullable because the player may disconnect before deferred delivery. A synchronous Before Event keeps
a required player.
The JavaScript name system.beforeEvents.scriptUnload and Python name system.before_events.script_unload both denote
one special Script Lifecycle Event rather than an ordinary native Before Event. The signal runs synchronously before
final Script Run cleanup, cannot cancel teardown, and permits direct World Mutation. See
Script lifecycle for its full contract and restrictions.
Scheduler
system.run(callback)runs in the next available final phase; a call made from a System task targets the following tick;- JavaScript
system.runTimeout(callback, ticks)/ Pythonsystem.run_timeout(callback, ticks)runs once after a delay;0may run again in the current tick, even from another System task; - JavaScript
system.runInterval(callback, ticks)/ Pythonsystem.run_interval(callback, ticks)repeats on an interval; - JavaScript
system.runJob(generator)/ Pythonsystem.run_job(generator)advances a generator incrementally; - JavaScript
system.waitTicks(ticks)/ Pythonsystem.wait_ticks(ticks)returns a Promise/awaitable that completes after the tick count.
If the previous interval occurrence is queued or running, the next schedule point is skipped. This is part of the QuestScript scheduler contract. Subscriptions and tasks created by a Script are cleaned when it unloads.
clearRun/clear_run and clearJob/clear_job also cancel a callback or generator step that has already moved into
the executor queue when guest execution has not started. They do not interrupt a callback or step that is already
running. Clearing an interval still removes all future schedule points, but it does not cancel detached asynchronous
work that its callback already started.
Ordinary scheduler and event callbacks have a synchronous void/None contract. A JavaScript callback may start an
async function, but QuestScript does not await the returned Promise, handle its rejection, or associate it with
clearRun/clear_run or the subscription. The next interval invocation does not wait for that Promise. A continuation
after system.waitTicks is serviced as detached work through the shared continuation queue. Handle failures inside the
asynchronous task itself. A Before Event decision, including cancel, must be made before the handler returns
synchronously. Only an API with an explicitly declared Promise/awaitable callback contract may await its result.
After ordinary game logic, the runtime services the bounded lifecycle queue, then ready runJob/run_job steps,
Promise/awaitable continuations, System tasks, and grouped After Events. It checks continuations after every System task
and completed event-type group. The phase and queues are bounded, so recursive
runTimeout(..., 0)/run_timeout(..., 0) cannot obtain an unbounded tick.
An ordinary callback may be deferred when the tick's soft budget is exhausted. After waiting for 10 ticks it enters its work kind's overdue lane: no more than 8 such entries are admitted per tick, and only while the global soft budget remains. This raises priority without forcing the whole accumulated backlog into one tick. Future scheduler registrations and ready units together are bounded to 4,096 slots globally and 512 per Script Run; a pending interval occurrence or job step counts separately from its retained registration. New work beyond a limit is rejected explicitly. Unload, reload, shutdown, or quarantine completes every remaining outcome exceptionally exactly once.
Server-thread budget
The ordinary final-scripting-phase budget uses time already spent by the game before that phase and the median of the last 10 such measurements multiplied by 1.2. The target tick is 50 ms, ordinary Script work receives between 15 and 35 ms, and a fixed 1 ms reserve remains. If 40 ms has already elapsed before the final phase, no ordinary callback starts in that tick; lifecycle cleanup retains a separate 5 ms soft reserve.
The soft deadline controls only whether the next unit starts. An admitted JavaScript/Python callback is not suspended between ticks and may extend the tick until it completes or the hard watchdog interrupts it. These settings therefore do not guarantee a tick shorter than 50 ms. The selected prototype still recorded 18 ticks longer than 50 ms across 16,000 measured ticks; the team accepted that as residual risk. Callback-duration prediction and separate Script/entity budgets are deferred to post-release work.
Operators may override the starting values through JVM properties with the questscript.serverThreadBudget. prefix:
targetTickMillis, minScriptBudgetMillis, maxScriptBudgetMillis, fixedReserveMillis, gameHeadroomMultiplier,
historyWindowTicks, emergencyPreFinalCutoffMillis, lifecycleReserveMillis, overdueAfterTicks,
maxOverdueEntriesPerKindPerTick, maxLifecycleQueue, maxOrdinaryQueue, maxOrdinaryQueuePerScript,
maxAfterEventOccurrences, maxAfterEventOccurrencesPerScript, maxLifecycleEntriesPerTick, and
maxGuestEntriesPerTick. Invalid combinations are rejected at startup. Scripts cannot change these values.
For routine inspection use /qs diagnostics threading [scriptId]. This permission-level-2 operator command returns a
bounded JSON snapshot of the current budget, queues, overdue age, interval skips, overflow, watchdog, and lifecycle
failures attributed to Script Runs. A global response includes at most 64 Script Runs and 64 pending-work samples, and
each Script Run includes at most 32 intervals; *Truncated fields report clipping. The snapshot never enters a Graal
Context and contains no Script source, arguments, event payloads, or results.
For deep recording enable -Dquestscript.optionA.diagnostics=true. The questscript.OptionAPhase,
questscript.OptionAAdmission, and questscript.OptionAGuestUnit JFR events expose the calculated budget, input
measurements, emergency state, queue depths, deferral age, overflow, Script Run, and watchdog outcome. Use the exact
trace recorder to reconstruct complete tick timelines. Soft admission exhaustion defers work that has not started; the
hard watchdog interrupts an already running unit and cannot reclaim time already spent in the current tick.
The global JavaScript setTimeout, setInterval, clearTimeout, and clearInterval functions are not available. All
delays use ticks through JavaScript system.runTimeout, system.runInterval, system.waitTicks or Python
system.run_timeout, system.run_interval, system.wait_ticks; the value selects the
earliest eligible tick rather than an exact wall-clock instant. Waiting never blocks the server thread.
Script Events
/scriptevent <namespace>:<path> [message] delivers a broadcast event through JavaScript
system.afterEvents.scriptEventReceive or Python system.after_events.script_event_receive.
The namespace is an author-chosen messaging channel. It is not derived from a Script ID, does not identify the sender,
and does not depend on how a Script was loaded. When the optional message is omitted, the handler receives an empty
string.
At present only the operator /scriptevent command can publish these events; there is no Script-side
JavaScript system.sendScriptEvent / Python system.send_script_event method yet.
The complete identifier remains in event.id, while namespaces compares only
the portion before :. JavaScript passes an options object:
signal.subscribe(handler, { namespaces: ["arena"] }); Python passes a keyword:
signal.subscribe(handler, namespaces=["arena"]). This filter receives
arena:round_start and arena:admin/reset, but not lobby:round_start. An
omitted filter or namespaces: null / namespaces=None receives every valid
Script Event; an explicit empty list receives none.
The JavaScript fields sourceEntity, sourceBlock, dimension and corresponding Python fields source_entity,
source_block, dimension contain QuestScript Public Facades (Entity, Block, and Dimension) rather than native
Minecraft Java objects. When a live source is unavailable, the corresponding field is null; stable JavaScript fields
id, sourceKey, sourceEntityId, position and Python fields id, source_key, source_entity_id, position
remain available. The entity ID field is the UUID of an Entity source and is null for other source types.
A Script Event ID uses the Minecraft <namespace>:<path> form with explicit non-empty parts. An invalid ID is rejected
before the event is published.
See the API reference for the exact event and DTO inventory.