Skip to main content

Event Handlers

The on.* namespace is how your component responds to things. Each hook registers a Roblox event connection (or signal subscriber) that Pulse manages and cleans up automatically.

The mental model is SolidJS createEffect — scoped to a specific Roblox event.


Frame events

on.heartbeat — every frame

on.heartbeat(() => {
// runs ~60 times per second (RunService.Heartbeat)
})

With options:

on.heartbeat({ when: enabled }, () => {
// skipped entirely when enabled() === false
})

on.heartbeat({ when: enabled, every: 0.1 }, () => {
// 10 times/sec instead of 60
})

on.heartbeat({ every: interval }, () => {
// interval is a signal — throttle adapts at runtime
})

Use for logic that needs to run every frame. Throttle with every if you don't need 60fps frequency — most ESP and detection loops are fine at 10fps.

on.renderStepped — before camera renders

on.renderStepped({ when: enabled }, () => {
// runs before each frame is rendered (RunService.RenderStepped)
})

Use for anything that affects what the player sees — camera manipulation, drawing updates, aimbot lock.

on.stepped — physics step

on.stepped(() => {
// RunService.Stepped — physics-related logic
})

Less common than heartbeat. Use when you need to align with Roblox's physics simulation step.


Character events

on.respawn / on.characterAdded

on.respawn(() => {
// fires when LocalPlayer.CharacterAdded
const h = _PulseGetHumanoid()
if (h) h.WalkSpeed = enabled() ? speed() : 16
})

on.characterAdded(() => {
// same as on.respawn
})

on.characterRemoving(() => {
// fires just before the character is destroyed
})

Use on.respawn to re-apply state after the player dies. The character helpers (_PulseGetChar, _PulseGetHumanoid, etc.) always reflect the current character — no caching needed.


Input events

on.inputBegan / on.inputEnded

on.inputBegan((input, gpe) => {
if (gpe) return // Roblox already handled it (typing in chat, menus)
if (input.KeyCode === Enum.KeyCode.E) {
// E was pressed
}
})

on.inputEnded((input, gpe) => {
if (input.UserInputType === Enum.UserInputType.MouseButton2) {
// right mouse button released
}
})

Always check gpe unless you want the handler to fire even when the user is typing.


Signal change

on.signal(enabled, (v) => {
// fires synchronously whenever enabled changes
if (v) startLoop()
else stopLoop()
})

Watch multiple signals:

on.signal(fov, applyFov)
on.signal(enabled, applyFov)

This fires synchronously — the handler runs before the .set() call returns. Keep handlers fast; use task.spawn for any async work.


One-shot delay

on.after(2.5, () => {
// runs once, 2.5 seconds after load
// automatically cancelled if the script is destroyed first
})

Equivalent to task.delay(2.5, fn) but lifecycle-aware.


when — conditional guard

on.heartbeat({ when: enabled }, () => {
// entire handler is skipped when enabled() === false
})

This is the core Pulse pattern. Feature logic only runs while the feature is active. Compiles to an early-return guard at the top of the handler.


every — throttle

on.heartbeat({ when: enabled, every: 1.0 }, () => {
// at most once per second
})

on.heartbeat({ every: interval }, () => {
// interval is a PulseSignal<number> — throttle adapts at runtime
})

Character helpers

Inside any on.* handler, use these globals instead of caching character references:

HelperReturns
_PulseGetChar()Character Model or undefined
_PulseGetHRP()HumanoidRootPart or undefined
_PulseGetHumanoid()Humanoid or undefined
_PulseGetAlive()booleantrue if humanoid.Health > 0

These read live from LocalPlayer.Character on every call. Never cache the result in a variable between calls — it becomes stale the moment the character is replaced.

// WRONG — stale after respawn
const cachedH = _PulseGetHumanoid()
on.heartbeat({ when: enabled }, () => {
if (cachedH) cachedH.WalkSpeed = speed()
})

// RIGHT — always read fresh
on.heartbeat({ when: enabled }, () => {
const h = _PulseGetHumanoid()
if (h) h.WalkSpeed = speed()
})