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, with your automations at the root
- 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.
When AI is available for your organization, the Companion in the sidebar can write or change actions, triggers and shared scripts for you: every change shows up as a diff you accept or reject.
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 // { username: "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 === trueimport { event } from "rawtoh"
// A module cannot `return` at the top level. For a multi-step condition,
// put the checks in a function and export its result.
function matches() {
const p = event.payload
if (!p.is_mod && !p.is_broadcaster) return false
if (!p.message.startsWith("!poll ")) return false
return p.message.includes("|")
}
export default matches()Writing 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), .set(key, value) and atomic .update(key, fn) |
log(...args) | Log output visible in the Activity panel |
sleep(ms) | Pause execution for the given duration |
exit() | Stop the action here and mark the process successful. A top-level return is not allowed in a module; this is the early return. It cannot be caught, and finally blocks do not run |
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", {
sceneName: "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.display_name
const plan = event.payload.plan_name
// Bump the sub count in storage (atomic, safe under concurrent subs)
const count = await storage.update("sub_count", (n) => (n ?? 0) + 1)
// Send a chat message
await module("twitch").request("chat.say", {
message: `Welcome @${user}! You're subscriber #${count} (${plan})`
})
log(`New sub: ${user} (${plan}), 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.username)
})// 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. - Use
storage.update()for counters — agetfollowed by asetcan lose increments when two actions run at once.