AI token DSL reference
Token syntax overview
AI command response templates support the same $(…) token syntax as Chatters commands. The following tokens are available:
| Token | Description |
|---|---|
$(user) | Display name of the triggering viewer |
$(touser) | First argument, or empty |
$(channel) | Channel login name |
$(query) | Full message after the trigger |
$(args) | All arguments joined with spaces |
$(1), $(2), … | Individual positional arguments. The brackets are required — a bare $1 is ordinary text, so a response can quote a dollar amount |
$(count) | Times this command has been used |
$(uptime) | Stream uptime |
$(game) | Current game/category |
$(title) | Current stream title |
$(random MIN-MAX) | Random integer, e.g. $(random 1-20) |
$(counter NAME [op N] [of TARGET]) | Persistent per-user integer counter |
$(eval EXPR) | Sandboxed expression (see below) |
$(counter …) — persistent counters
Counters are the only persistent storage available in command templates — a whole-number tally saved per user, per channel.
Counter syntax
$(counter NAME) → read triggering user's counter (0 if unset)$(counter NAME add N) → add N; returns new total$(counter NAME sub N) → subtract N; returns new total$(counter NAME set N) → set exact value; returns new value$(counter NAME top) → leaderboard of top users (highest first)$(counter NAME top 5) → leaderboard capped at 5 entries$(counter NAME add N of TARGET) → apply to a specific user$(counter NAME add N of channel) → channel-wide counter (one shared number)$(counter NAME $2 $3 of $1) → op/amount/target all from chat argumentsNAME must be lowercase letters, digits, or underscore (max 32 chars).
N is a whole number or a positional argument $1, $2, …
op (add/sub/set) may also be a $N argument so one command handles multiple actions.
Never nest $(…) inside a $(counter …) token — use $1/$2 for arguments and a literal name or channel for the target.
Counter examples
// Bits-owed tracker — !owe <amount> accumulates across messages{"trigger":"!owe","response_template":"😤 $(user) now owes the stream $(counter bitsowed add $1) bits!","cooldown_s":0,"user_level":"everyone"}
// One mod command that adds, subtracts, OR sets — action chosen in chat{"trigger":"!points","response_template":"$(touser)'s points: $(counter points $2 $3 of $1)","cooldown_s":0,"user_level":"mod"}
// Death counter — channel-wide shared number{"trigger":"!death","response_template":"☠️ Death #$(counter deaths add 1 of channel)!","cooldown_s":0,"user_level":"mod"}
// Leaderboard — list top tariff dodgers{"trigger":"!tariffboard","response_template":"🏆 Top tariff dodgers: $(counter tariffs top)","cooldown_s":5,"user_level":"mod"}$(eval …) — sandboxed expressions
Evaluates a sandboxed JavaScript-like expression. The result is inserted into the response. $(eval) is stateless — it cannot store values between messages (use $(counter …) for that).
Supported syntax
| Construct | Examples |
|---|---|
| Literals | 42, "hello", 'world', true, false, null |
| Operators | `+ - * / % == != < > <= >= && |
| Ternary | cond ? a : b (chain by nesting in the alt branch) |
| Let bindings | let NAME = EXPR; EXPR (chain with ;) |
| Built-in functions | random(min, max), parseInt(s), parseFloat(s), Number(x), String(x), upper(s), lower(s), trim(s), len(s), at(s, i), split(s, sep), join(arr, sep), replace(s, from, to) |
| Math | Math.floor(x), Math.ceil(x), Math.round(x), Math.abs(x), Math.min(…), Math.max(…), Math.random() |
| Context (read-only) | user, touser, channel, query, args |
| Argument access | at(args, 0) (first arg), at(args, 1) (second arg), … |
Not supported in $(eval)
var,const,function,=>,return,if,while,for,try- Bare assignment (
x = 5) — uselet - Bracket indexing (
args[0]) — useat(args, 0) - Invented identifiers — only
user,touser,channel,query,argsexist - Nesting
$()tokens inside eval — the only exceptions are the reads swapped for their value before the expression runs:$(counter NAME [of TARGET])(no op),$(plus [FIELD]),$(followers),$(subs)and$(subpoints) - Any token inside a quoted string —
'level $(plus level)'comes out literally. Bind it first, then concatenate
Reading a counter inside $(eval)
Embed a bare counter read (no add/sub/set) inside the eval expression. It is replaced by the current integer before evaluation:
// !tariffs @user — branches on how much the named viewer owes{"trigger":"!tariffs","response_template":"$(eval let v = $(counter tariffs of $1); v == 0 ? '✨ ' + touser + ' owes nothing!' : '🚨 ' + touser + ' owes ' + v + ' bits!')","cooldown_s":0,"user_level":"everyone"}To change a counter from the same command, put the $(counter … add/sub/set …) token elsewhere in the response text — never inside $(eval).
Branching on a channel stat inside $(eval)
$(plus [FIELD]), $(followers), $(subs) and $(subpoints) are substituted the same way, so a
response can react to how far the channel is from a goal instead of just printing the number:
// !plus — "X points, Y more to reach Level N" while there's a gap, "X / target" once there isn't{"trigger":"!plus","response_template":"$(eval let lvl = $(plus level); let pts = $(plus points); let need = $(plus needed); let goal = $(plus next); need > 0 ? '💜 Plus Level ' + lvl + ' — ' + pts + ' points, ' + need + ' more to reach Level ' + (lvl + 1) + '!' : '💜 Plus Level ' + lvl + ' — ' + pts + ' / ' + goal + ' points!')","cooldown_s":15,"user_level":"everyone"}Every one of these is a whole number except $(plus share), which is text like 60/40.
Two rules matter here:
- Keep the tokens outside quoted strings. String literals are copied verbatim, so
'Level $(plus level)'posts that token to chat as written. Bind it (let lvl = $(plus level)) and concatenate. - Don’t write your own fallback for a missing figure. If Twitch can’t report it, the whole
response becomes
[plus: unavailable]rather than evaluating with a0— a0would make the “you need N more points” branch state a fault as a fact about the month.
$(eval) examples
// D20 dice roll with branching result{"trigger":"!roll","response_template":"$(eval let r = random(1, 20); r == 20 ? '🔥 CRIT HIT! ' + user + ' rolled 20!' : r == 1 ? '💀 CRIT MISS! ' + user + ' rolled 1!' : r >= 12 ? '✅ Hit! ' + user + ' rolled ' + r : '❌ Miss! ' + user + ' rolled ' + r)","cooldown_s":5,"user_level":"everyone"}When AI Generate says it can’t
Some descriptions can’t be turned into a command at all — not because the generator is having a bad day, but because nothing in this syntax does what they ask. The usual cases:
- Remembering text.
$(counter …)stores a whole number per viewer. There is no store for a name, a quote, a list, or per-item history. - Acting on chat. A command’s response is a chat message. Timing someone out, banning, changing a reward, or sending a DM are not things a response can do.
- Data no variable provides. If it isn’t in the token table above, a command can’t read it —
though
$(urlfetch …)can pull from an HTTPS endpoint you control.
In those cases AI Generate answers with a short explanation of what’s missing and the closest thing it can build, instead of producing a command that quietly does the wrong thing. If it can’t build a valid command for some other reason, it tells you the specific problem it kept hitting — rephrase around that rather than re-running the same description.
Share Feedback
Need help or want to chat? Join our Discord to get support, report bugs, and talk with other streamers.▸ What we're sending
| Module | — |
| Page | — |
| Browser | — |
| Screen | — |
| Channel | anonymous |
| Submitted | — |