Run script
Execute custom TypeScript in an action handler chain with full app API access and typed trigger context.
Run script
| Handler ID | core:core:script:run-script |
| Plugin | Core Handlers |
| Category | Script |
Run custom TypeScript inside an action handler chain. Scripts receive the full Stream Kit app API (same as plugins) and the trigger context for the current run. The in-app editor uses Monaco with TypeScript IntelliSense; use Open in editor to edit the script in Cursor or VS Code with auto-sync back to Stream Kit.
When to use
Use Run script when built-in handlers are not enough: custom logic, combining multiple plugin APIs, transforming trigger data, or setting action variables programmatically.
Script shape
Export a default async function that receives a single ScriptContext object:
import type { ScriptContext } from '@stream-kit/script-api';
export default async ({ app, context }: ScriptContext) => {
const [{ trigger, data, actionVariables }] = context;
await app.toast.create({
title: `Triggered by ${trigger}`,
variant: 'success'
});
};| Parameter | Type | Description |
|---|---|---|
app | PluginAppApi | Full application API — toast, filesystem, plugins, actions, queues, and more. See Plugin API. |
context | HandlerTriggerContext[] | Trigger payloads for this run (usually one entry). |
context entries
Each item in context has:
| Field | Type | Description |
|---|---|---|
trigger | string | Stable trigger ID (for example twitch:twitch:chat:chat-message). |
data | Trigger-specific | Payload from the trigger that fired. Typed in the editor based on triggers configured on the action. |
actionVariables | Record<string, string> | Mutable action-scoped variables for this handler chain run. Changes are visible to later handlers. |
You can also return a plain object from the script. Each key is written into actionVariables (values are stringified) so later handlers can use {key} placeholders.
Browse trigger payloads in the Triggers API reference.
app API overview
Scripts use the same app object as plugin handlers:
| API | Purpose |
|---|---|
app.toast | Show notifications |
app.confirm | Ask the user to confirm |
app.fs | Read/write files |
app.plugins | Call other plugins' public APIs |
app.actions | Run or refresh actions |
app.actionQueues | Pause, resume, or inspect queues |
app.commands | Integrate with chat commands |
app.oauth | OAuth flows |
app.opener | Open URLs or paths |
app.process | Watch or run programs |
app.hotkeys | Global shortcuts |
app.localTts | Local TTS runtime |
app.db | Plugin database migrations |
app.i18n | Translations |
See the full reference in Plugin API.
Configuration
| Field | Type | Required | Description |
|---|---|---|---|
| Script | code (TypeScript) | Yes | Script body. Default template exports async ({ app, context }) => { … }. |
Editor and types
- In-app editor — Monaco with TypeScript checking and autocomplete for
app,context, and triggerdata(narrowed to triggers on the action). - Open in editor — Writes a small project under app data (
scripts/actions/{actionId}/handlers/{handlerId}/) and opens it in Cursor or VS Code. File changes sync back to the handler field automatically.
Save the action first; Open in editor requires a persisted action ID.
Limits and errors
| Limit | Value |
|---|---|
| Execution timeout | 5 seconds |
| Error handling | Failures show an error toast; the handler chain continues |
Thrown errors and timeouts do not stop the handler chain unless a later handler depends on side effects that did not run.
Examples
Show a toast with trigger data
import type { ScriptContext } from '@stream-kit/script-api';
export default async ({ app, context }: ScriptContext) => {
const [{ data }] = context;
const message = typeof data === 'object' && data && 'message' in data
? String((data as { message: unknown }).message)
: 'Action ran';
await app.toast.create({ title: message, variant: 'info' });
};Set an action variable for later handlers
Mutate actionVariables in place, or return a plain object — both become {key} placeholders for later handlers. Objects/numbers are stringified.
import type { ScriptContext } from '@stream-kit/script-api';
export default async ({ context }: ScriptContext) => {
const [{ data, actionVariables }] = context;
if (actionVariables) {
actionVariables.greeting = 'Hello from script';
}
// Equivalent: return values are merged into action variables
return {
normalizedMessage:
typeof data === 'object' && data && 'message' in data
? String((data as { message: unknown }).message).toLowerCase()
: ''
};
};Use {greeting} or {normalizedMessage} in subsequent handler fields.
Call a plugin API
import type { ScriptContext } from '@stream-kit/script-api';
export default async ({ app, context }: ScriptContext) => {
const twitch = app.plugins.tryGet<{ sendMessage(channel: string, message: string): Promise<void> }>('twitch');
const channel = typeof context[0]?.data === 'object' && context[0].data && 'channel' in context[0].data
? String((context[0].data as { channel: unknown }).channel)
: '';
if (twitch && channel) {
await twitch.sendMessage(channel, 'Script says hi!');
}
};