RawtohRawtoh/Docs
Documentation

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:

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

ImportDescription
eventThe event object (name, payload, emitter_group, emitter_name)
triggerThe 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 true
import { 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 === true
import { 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

ImportDescription
eventThe event that triggered this execution
triggerThe trigger that matched
module(group, name?)Returns a proxy with .request() and .notify()
storageKey-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 alert

You can enable or disable entire folders at once — handy for turning off a group of automations without deleting them.

Tips