Skip to main content
Your code sits between your input devices and your computer: every keypress, mouse move, and scroll can be read, transformed, blocked, or replaced. Scripts are written in Luau; where the input comes from and where the output goes is covered in How it works. This guide teaches the model and the common patterns. For the exact parameters of every function, see the SDK reference; to run your first script, see the quickstart.

Lifecycle

Every script follows a predictable lifecycle:
You implement behavior by defining hooks, global functions the runtime calls when something happens:
  • OnStart() / OnStop(): set up and tear down (start timers, cancel tasks, release keys).
  • OnFocus(window) / OnBlur(window): fire when a targeted app gains or loses focus (see Targeting).
  • OnTick(dtMs): runs on a fixed cadence while the script is active; the tick rate, dtMs semantics, and limits are in the SDK reference.
  • OnDown / OnUp / OnMove / OnScroll: input events, covered next.
A script with no targeting is always active, so OnFocus/OnBlur never fire. See Hooks for the exact signatures.

Input and blocking

Input hooks fire when physical input arrives from your captured devices. Their return value decides whether your PC sees the input:
  • return true: input passes through normally.
  • return false: input is swallowed; the PC never sees it.
  • no return / return nil: same as return true.
This is how remapping works: block the original key, then send a different one with HID.
Bind is the declarative shortcut for simple remaps and uses the opposite default: it blocks unless you return true. See Bind.
Prefer Bind when you only care about specific keys. Defining OnDown or OnUp makes Rebind intercept every key system-wide; the SDK reference covers this exclusive-capture behavior and the key_block=false opt-out. Mouse moves are handled differently, and they need hardware. Any script that defines OnMove requires a Rebind device: Rebind refuses to load an OnMove script in software mode; The Rebind Link explains why motion can’t be intercepted from software. (Key, button, and scroll blocking work in both modes; only mouse movement is hardware-gated.) On a device, OnMove fires asynchronously by default: the move is forwarded to your PC immediately and the return value is ignored, so script time never sits in the mouse path. To modify or swallow moves (acceleration curves, cursor smoothing), add mouse_block=true to the modeline; then the return value matters:
Keys are identified by case-insensitive string names ("A", "LCtrl", "Mouse1", …). The complete list (every alias and which keys are input- or output-only) is in the Key reference.

Sending output

The HID namespace sends keyboard and mouse output through the active transport; it appears as real input to your PC.
  • HID.Down / HID.Up hold and release a key across time.
  • HID.Combo fires a whole chord in one call; HID.Press taps a key (optional hold in ms); HID.Type sends a string through the active transport in one call.
  • Use + for modifier combos: HID.Combo("LCtrl+V") presses in order and releases in reverse.
HID.Combo, HID.Down, and HID.Up are non-blocking and safe anywhere, including an input hook or a Bind action. HID.Type needs no coroutine, but hardware typing emits per-character reports and can occupy the calling hook. HID.Press and HID.Typewriter use an internal Sleep, so they must run inside a Run() coroutine or an Async() handler.
HID.Type submits text in one call. Character coverage and timing depend on the active transport and keyboard layout; see Transport key support. For a controlled per-character delay, use HID.Typewriter(text, delayMs?) (coroutine only). For very long or arbitrary Unicode text, paste is still an option where Clipboard is supported:
The full output surface (mouse movement, scroll, absolute mode) is in the HID reference.
Software-mode gotcha: a physical modifier the user is holding is merged into your synthetic output: a swallowed key re-emitted while Shift is down still carries Shift. That’s correct for remaps, but a macro that types text while a modifier is held will inherit it. For output that must be clean of physical modifiers, gate on Input.GetModifiers() first. Hardware output is not a general modifier-isolation guarantee.

Doing things over time

For sequences, delays, and loops, use Run() and Sleep():
Run() launches a coroutine that executes concurrently; Sleep() pauses that coroutine without blocking anything else, and is only valid inside a Run() block. Reach for:
  • After(ms, fn): a one-off delay without the Run/Sleep boilerplate.
  • Async(fn): wrap a callback so it runs in a coroutine; use it for Bind callbacks that need Sleep.
  • Timer.After / Timer.Every: simple delayed or repeated callbacks, when you don’t want to manage a coroutine loop yourself.
Coroutines are cooperative and tick-driven. Sleep resumes on the next engine tick past its deadline, so its timing is tick-granular rather than exact milliseconds, and a Run() body that never calls Sleep runs straight through on a single tick. An infinite loop with no Sleep stalls the script (input freezes) until the runtime’s per-tick budget (~200 ms) cuts it off. Always put a Sleep inside long-running loops.
Run() returns a handle (task:Cancel(), task:IsRunning()). Cancel long-running tasks in OnStop/OnBlur so they don’t leak:
Exact signatures are under Globals and Timer.

Reading state

When you need to know what’s happening right now rather than waiting for an event, poll it:
  • Input reports currently held keys, modifiers, and how long a key has been down, useful inside OnTick or a Run() loop.
  • System gives the current time, cursor position, screen size, and focused window, refreshed each tick.
See Input and System for the methods; the Double-tap pattern below shows System.Time in use.

Config panels

A script can define a settings panel that appears in the Rebind UI, so users tune it without editing code. UI.Schema declares the controls; you read and write values through the returned handle, and they persist automatically (keyed to the script’s file path):
The Config tab for a running script showing a trigger keybind, radius, duration, and smoothness sliders, and an auto-start toggle

A script's Config tab, rendered from its UI.Schema: keybind, sliders, and an auto-start toggle. Changes apply live.

The full widget catalog (toggles, sliders, keybinds, selects, text, plus layout and notifications) is in the UI reference.

Splitting across files (require)

As a script grows, pull shared logic into its own file and load it with require(). A module is just a .luau / .lua file that returns a value, usually a table:
require resolves relative to the script’s own directory, dots are path separators (require("lib.math") loads lib/math.luau), modules are cached after the first load, and anything outside the script’s folder is rejected: local files only, no external packages. The exact resolution order and sandboxing rules are in the require reference.
Only the script you run needs a min_sdk modeline; a require’d module needs none. See Modeline.

Macros

Macro.Play plays back a recorded input sequence: a table of move/press/scroll/sleep actions. Reach for a macro when you have a fixed sequence to replay; use raw HID calls when the output depends on logic. Patterns can be transformed before playback (Math.Scale, Math.Spline, Math.Resample). See the Macro reference for the action format and playback modes.

Targeting and the always-on model

Restrict a script to specific applications with modeline targeting. window= matches a case-insensitive substring of the focused window’s title (so window=code also matches “Visual Studio Code”); process= matches the process name the same way. Repeat either key on one line or across several lines. Positive window and process rules combine with OR:
Use window_regex= and process_regex= when one expression is clearer than several literals. Regular expressions are case-insensitive. Rust regex syntax applies, so lookaround and backreferences are not supported:
Exclusions deactivate a script when they match. They always win over positive rules. With no positive rules, an exclusion-only config means “active everywhere except here”:
Regex and exclusion targeting require min_sdk=3.4.1 or newer. This makes an older Rebind build refuse the script instead of ignoring targeting keys it does not understand. OnStart still runs when a targeted script loads. While its target does not match, input hooks, timers, coroutines, and OnTick stay inactive. Only scripts matching the focused window process input:
Scripts without targeting are always active. Use them for system-wide behaviors like remaps and media controls. Invalid or empty regular expressions stop a script from loading instead of widening its scope. When several scripts run at once, z_index controls which sees input first (a false return blocks lower-priority scripts), and instance controls what happens when a script loads while a copy is already running. Full targeting rules and every key are in the Modeline reference.

Patterns

Common shapes that compose the pieces above.

Toggle

Hold loop

Double-tap

Style

Rebind scripts are Luau. The conventions every example in these docs follows:
  • Indent with 2 spaces. Never tabs, never 4.
  • Naming: runtime hooks and SDK calls are PascalCase. That is dictated, not a choice (OnDown, HID.Press, Input.IsDown). Your locals and local functions are snake_case (dwell_start, local function set_virt). Module-level constants are UPPER_SNAKE. The UI config table is conventionally named cfg.
  • local everything. The only globals are the runtime hooks (OnStart, OnDown, …); declare everything else local, including functions.
  • Double-quoted strings. Use [[ ]] long strings for regex patterns and multi-line text.
  • Stay flat: early return. Guard and return rather than nesting; use a and b or c for a ternary.
  • Types are optional but welcome. Luau is typed; annotate where it clarifies (local count: number = 0), don’t annotate the obvious.
  • Comments: explain why, not what, and stay sparse. Lowercase. -- for a single line; one --[[ ]] block instead of stacking several -- lines.
  • Keep lines to roughly 80 characters; space after commas and around operators; one statement per line.

Validation

The runtime lints your script on load and warns (in the Logs tab, without blocking the load) about common issues:
  • No hooks defined: no hook and no Bind was registered, so the script loads but never runs any logic.
  • Net.Get/Net.Post in a script with OnTick and a tick_rate above 1000: an HTTP request per tick at that rate is almost always a mistake. Move it inside Run() (where the client methods must run anyway) or onto a Timer with a longer interval. WebSocket handlers (Net.WSListen/Net.WSConnect) are exempt from the Run() requirement: their I/O runs on dedicated threads.

Type checking

The in-app editor includes Rebind SDK autocomplete and hover information. For VS Code or another external editor, follow Editor setup to load the same rebind.d.luau definitions into the Luau Language Server.