Events
Announce something happened and start an automation.
Everything so far starts with a button. Events are the other direction: something happens out in the world — a chat message, a follow, a scene change — and the user has an automation waiting for it.
A plugin's job is to announce those happenings. What runs in response is the user's business: they create an Event, pick the Event Source as what fires it, optionally filter it, and choose the Actions to run.
Declare what you can announce
{
"events": [
{
"type": "chat-message",
"name": "Chat message",
"fields": [
{ "key": "user", "name": "Username", "type": "string" },
{ "key": "text", "name": "Message", "type": "string" },
{ "key": "isMod", "name": "From a moderator", "type": "boolean" }
]
}
]
}name is what the user picks from a list. type is the identifier passed to
emitEvent.
Fields are the important part. They are what the user can filter on and what later steps can read. An Event with no fields can only ever mean "this happened", which is rarely enough — a chat message whose text nobody can read cannot do much.
Announce it
chat.on("message", (message) => {
plugin.emitEvent("chat-message", {
user: message.author,
text: message.text,
isMod: message.badges.includes("moderator"),
});
});That is the whole API. It is fire-and-forget: nothing is awaited, and the plugin learns nothing about what ran, or whether anything did.
Two things are dropped silently:
- an event
typenot declared inevents[] - a payload key not declared in that Event Source's
fields[]
A field declared but absent from the payload is simply missing for that firing.
Types are checked against the declaration too, so isMod must be a real boolean,
not "true".
What users do with the fields
Filter
The user can narrow an Event: only messages from a moderator, only from a
specific channel. That is why field types matter — a boolean field gives a
clean toggle, a number gives "greater than".
A filter's value can be a dropdown of live choices, with operand options on the field:
{
"key": "channel",
"name": "Channel",
"type": "string",
"operand": { "editor": "select", "options": "channels" }
}Read them in the Actions
Inside that Event's Action list, every field is readable as a
Run Variable, addressed
<plugin>.<event-type>.<field-key>:
{{twitch.chat-message.user}}A user can therefore build "when a chat message arrives, say
hi {{twitch.chat-message.user}}" with no further work on the plugin's side.
Declaring good fields is what makes that possible, and it is where most Run
Variable references come from.
Running without a button
An Event's Action list may have no button attached at all, which is the
practical argument for preferring StandaloneAction: an ItemAction is only
usable in automations bound to a button, so declaring "context": "item"
without needing it narrows where the Action can be used.
Rate and volume
Nothing throttles emitEvent. A chat plugin emitting one Event per message in a
busy channel starts one Run per message, and unqueued Events run in parallel.
Managing that is the user's configuration, but a plugin can help:
- Do not emit what nobody can use. Every Event Source declared is one more thing in their list; every firing is potential work.
- Emit at a meaningful granularity. One
scene-changedper change, not one per property of the new scene. - Do not re-emit history on reconnect. A user does not want the last hour of chat replayed because a socket blipped.