Scripting
Write triggers and actions in TypeScript to power your automations.
Triggers and actions are written in TypeScript. Rawtoh runs your code in a secure, sandboxed environment every time a matching event arrives.
All runtime APIs are available via import { ... } from "rawtoh". You can also import shared scripts to reuse code across your automations.
The script editor
Rawtoh includes a built-in code editor with TypeScript autocompletion and syntax highlighting. The editor is split into three panels:
- Explorer — a tree view of all your triggers, actions, and shared scripts, organized by path
- Editor — the Monaco code editor where you write your scripts
- Activity — real-time process logs, events, and module call traces
You can have multiple scripts open in tabs and save with Ctrl+S. The editor provides autocompletion for "rawtoh" imports and shared script paths. You can Ctrl+Click on an import path to navigate to the shared script.
Writing trigger code
A trigger's code is a filter function. It receives the event and must use export default with a truthy value for the associated action to run. Trigger code has a 5-second timeout.
Available imports
| Import | Description |
|---|---|
event | The event object (name, payload, emitter_group, emitter_name) |
trigger | The trigger object itself |
Only event and trigger are available in triggers. log, sleep, module, and storage are not available — calling them will throw an error.
The event object
import { event } from "rawtoh"
event.name // "chat.message"
event.payload // { user_name: "alice", message: "!hello", ... }
event.emitter_group // "twitch"
event.emitter_name // "main-bot"Examples
// Always fire (pass-through trigger)
export default trueimport { event } from "rawtoh"
// Only fire for a specific command
export default event.payload.message === "!hello"import { event } from "rawtoh"
// Fire for subscribers only
export default event.payload.is_subscriber === trueWriting action code
An action's code is the business logic — what actually happens when the trigger fires. Actions run asynchronously with a 30-second timeout. Top-level await is supported.
Available imports
| Import | Description |
|---|---|
event | The event that triggered this execution |
trigger | The trigger that matched |
module(group, name?) | Returns a proxy with .request() and .notify() |
storage | Key-value store with .get(key) and .set(key, value) |
log(...args) | Log output visible in the Activity panel |
sleep(ms) | Pause execution for the given duration |
Calling module methods
Use the module() function to call methods on connected modules:
import { module } from "rawtoh"
// Send a Twitch chat message
await module("twitch").request("chat.say", {
message: "Hello from Rawtoh!"
})
// Change OBS scene (fire-and-forget)
await module("obs").notify("scene.set_current", {
scene_name: "BRB"
})
// Target a specific instance
await module("twitch", "alerts-bot").request("chat.say", {
message: "New follower!"
})Full example
import { event, log, module, storage } from "rawtoh"
// Welcome new subscribers with a custom message
const user = event.payload.user_name
const tier = event.payload.tier
// Get the sub count from storage
let count = await storage.get("sub_count") || 0
count++
await storage.set("sub_count", count)
// Send a chat message
await module("twitch").request("chat.say", {
message: `Welcome @${user}! You're subscriber #${count} (Tier ${tier})`
})
log(`New sub: ${user} (Tier ${tier}), total: ${count}`)Shared scripts
Shared scripts let you reuse code across multiple triggers and actions. They appear in the Explorer alongside your actions and triggers.
Use standard export and import syntax. Shared scripts can be imported by absolute path (from the tree root) or relative path:
Defining a shared script
// /utils/format (shared script)
export function greet(name) {
return `Hello, ${name}!`
}
export function upper(str) {
return str.toUpperCase()
}Importing shared scripts
import { event, log, module } from "rawtoh"
import { greet } from "/utils/format"
// Use shared function in your action
await module("twitch").request("chat.say", {
message: greet(event.payload.user_name)
})// Relative imports also work (from the current script's directory)
import { upper } from "./format"
import { double } from "../math/double" Shared scripts can also import from "rawtoh" and from other shared scripts. However, shared scripts imported by triggers must not use action-only APIs (log, sleep, module, storage) — calling them will throw a runtime error.
Organizing your scripts
Actions and shared scripts use a path-based hierarchy, like a file system. Group related automations into folders:
/twitch
/chat
/greet <- action: welcome new users
/commands <- action: handle !commands
/subs
/alert <- action: sub notification
/obs
/scenes
/auto-switch <- action: switch scenes automatically
/utils
/format <- shared: reusable formatting functions
/config <- shared: common configuration
/alerts
/donation <- action: donation alertYou can enable or disable entire folders at once — handy for turning off a group of automations without deleting them.
Tips
- Use the Activity panel to debug — it shows every event, process log, and module call in real time.
- Use
log()liberally — logs appear in the process details and help you understand what's happening. - Use cooldowns on triggers to prevent spam. Choose throttle (fires first, then waits) or debounce (waits, then fires).
- Start simple — an
export default truetrigger paired with alog(event.payload)action is a great way to explore what data a module sends. - Use shared scripts to avoid duplicating code across actions — extract common logic into
/utils/or similar. - Don't use
returnin actions — the return value is ignored. Usethrowto signal errors.