Skip to main content

Mental Model

Understanding a few core ideas before you write your first line of Pulse code will make everything click faster. This page is worth reading even if you're in a hurry.


Everything compiles to one file

Pulse is a compiler, not a runtime module. When you run rb build, it reads every .ts and .lua file in your src/ folder, transpiles and concatenates them, and writes a single .lua file.

src/combat/Aimbot.ts ──┐
src/player/AutoHeal.ts ──┤ → build/script.lua (one file)
src/visuals/ESP.ts ──┤
src/misc/globals.lua ──┘

What this means for you:

  • There is no require(). Everything is a Lua global after compilation.
  • Compilation order matters. Files are sorted by name within each folder.
  • globals.lua compiles first — put shared helper functions there.

How TypeScript and Lua files differ at compile time:

Plain .lua files (globals.lua, remotes.lua, etc.) are emitted flat — their locals are visible to later files as upvalues. This is how shared helpers and remote wrappers are accessible everywhere.

Every .ts component is compiled by typescript-to-lua into a Lua chunk and wrapped in its own isolated function scope (IIFE):

(function()
-- ... your component code
end)()

This means each component has its own local variable budget, so the script can grow to hundreds of components without hitting Lua's 200-local-per-function limit. There is no module system at runtime — it's still just one compiled file.


Components are just named sections of that file

When you write src/combat/Aimbot.ts, the compiler generates a defineComponent("Aimbot", ...) call and wraps your code inside it. At runtime, Aimbot is a global variable pointing to that component.

Every component's signals are accessible globally by other components:

Aimbot.enabled.set(true) // write its signal
const r = Aimbot.radius() // read its signal

Aimbot.enabled.watch((v) => { // subscribe from another component
if (v) showIndicator()
})

This is by design. Pulse scripts are small enough that global accessibility is a feature, not a problem.


Signals are the spine of your script

A signal is a reactive variable. When it changes, anything subscribed to it updates automatically. This includes:

  • The UI toggle or slider bound to it
  • Any on.signal handler watching it
  • Any other component reading or watching it

You don't call "update the UI" or "notify the handler" manually. Write the new value and everything reacts.

enabled.set(true) // this one line:
// 1. updates the stored value
// 2. flips the toggle in the UI
// 3. fires on.signal(enabled, ...) handlers

The key insight: signals are the source of truth. Nothing else stores a copy.


on.* hooks are not if statements

When you write:

on.heartbeat({ when: enabled }, () => {
const h = _PulseGetHumanoid()
if (h) h.WalkSpeed = speed()
})

The compiler generates a RunService.Heartbeat:Connect(...) call. The { when: enabled } option adds a guard at the top of the callback that skips execution entirely when enabled() is false. The connection is tracked by the component and disconnected automatically when the script shuts down or re-runs.

You are not registering a conditional check — you are subscribing to a Roblox event with automatic lifecycle management.


What Pulse handles automatically

You writePulse handles
Your feature logicConnecting events to Roblox services
Signal default valuesDisconnecting everything on shutdown
Remote paths for your gameUI ↔ signal bidirectional binding
Team/classification logicLoop error isolation and logging
Your game's folder structureComponent registry (global by name)

If you find yourself writing Roblox boilerplate (Connect, Disconnect, manual cleanup) — that's a sign Pulse has a built-in for it.


The --dev build is a separate thing

When you build with rb build --dev, the output includes extra code: a floating dev overlay with a log viewer, feature toggles, and a config snapshot. This is only for your testing. The regular build output doesn't include any of it.

Always inject the --dev build while building and testing. Inject the regular build when you're done.


What Pulse does NOT handle

Be honest about what you still need to write:

  • The actual feature logic — Pulse can't write your aimbot math or your ESP visibility check
  • Remote event paths — every game is different; you supply the path strings
  • Team logic — you define what "enemy" means for your game
  • Game-specific folder structureworkspace.Titans.Alive, workspace.Shifters, etc. — you write those paths
  • Executor compatibility — Pulse is tested on common executors but some features (writefile, clipboard) depend on executor support

Understanding this boundary saves hours of "why isn't Pulse doing X" — X is probably your job, not Pulse's.