AI token DSL reference
Token syntax overview
Section titled “Token syntax overview”Chatters command responses — including the commands AI Generate drafts for you — are built from
$(…) tokens. The full set:
$(user), $(touser), $(channel), $(query), $(args), $(1), $(2), …, $(count), $(uptime), $(game), $(title), $(followage), $(twitch LOGIN [FIELD]), $(time TZ ["FORMAT"]), $(random MIN-MAX), $(randomchatter [FILTER]), $(randompick A|B|C), $(counter NAME [op N] [of TARGET]), $(eval EXPR), $(followers), $(subs), $(subpoints), $(plus [FIELD]), $(goal METRIC [FIELD]), $(subathon [FIELD]), $(tipgoal [FIELD]), $(charity [FIELD]), $(hypehigh FIELD), $(urlfetch URL), $(urlfetch json URL PATH)
Template variables & built-ins describes each one. This page covers the
two with a grammar of their own: $(counter …), the only storage a command has, and $(eval …), a small
sandboxed expression language.
Positional arguments are $(1), $(2), … — the brackets are required: a bare $1 is ordinary text, so a
response can quote a dollar amount. The one place a bare $1, $2, … means an argument is inside a
$(counter …) token, where they are that token’s own placeholders.
$(counter …) — persistent counters
Section titled “$(counter …) — persistent counters”Counters are the only persistent storage available in command templates — a whole-number tally saved per viewer in your channel, or one shared channel-wide number with of channel.
Counter syntax
Section titled “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 (per-viewer counters; highest first, 10 by default)$(counter NAME top 5) → leaderboard capped at 5 entries (25 at most)$(counter NAME closest N) → viewer(s) whose count is nearest N; every tie is listed$(counter NAME closest N under) → the same, but a count above N never counts$(counter NAME add N of TARGET) → apply to a specific user$(counter NAME of channel) → read the channel-wide counter (one shared number)$(counter NAME add N of channel) → add N to the channel-wide counter; returns new total$(counter NAME $2 $3 of $1) → op/amount/target all from chat argumentsNAME is letters, digits or underscore, up to 32 characters. Capitals are read as lowercase, so Deaths and deaths are one counter.
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. print and list work as top.
closest lists viewers with a count, lowest first when several tie, as name: value. A count of 0 is the same as no count, so it never matches. With nobody to list, it answers nobody yet.
TARGET is a viewer’s login (with or without @), a $N argument, or channel. Leave of TARGET out to use the viewer who ran the command.
Never nest $(…) inside a $(counter …) token — use $1/$2 for arguments and a literal name or channel for the target.
A response can hold at most 5 $(counter …) tokens.
Counter examples
Section titled “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"}
// Reading that death counter — the read needs "of channel" too{"trigger":"!deaths","response_template":"💀 Deaths so far: $(counter deaths of channel)","cooldown_s":5,"user_level":"everyone"}
// Leaderboard — list top tariff dodgers{"trigger":"!tariffboard","response_template":"🏆 Top tariff dodgers: $(counter tariffs top)","cooldown_s":5,"user_level":"mod"}$(eval …) — sandboxed expressions
Section titled “$(eval …) — sandboxed expressions”Evaluates a sandboxed JavaScript-like expression. The result is inserted into the response, cut to 400 characters; a mistake in the expression shows in chat as [eval: …]. $(eval) is stateless — it cannot store values between messages (use $(counter …) for that).
Supported syntax
Section titled “Supported syntax”| Construct | Examples |
|---|---|
| Literals | 42, "hello", 'world', true, false, null |
| Operators | + - * / % == != < > <= >= && || ! (use + for string concat) |
| 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), … |
Inside $(eval), touser is the first word after the trigger as typed, or empty when there isn’t one —
it does not fall back to the caller’s name the way the $(touser) token does.
Not supported in $(eval)
Section titled “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),$(subpoints),$(goal METRIC [FIELD]),$(subathon [FIELD]),$(tipgoal [FIELD])and$(charity [FIELD]) - Any token inside a quoted string —
'level $(plus level)'comes out literally. Bind it first, then concatenate
Reading a counter inside $(eval)
Section titled “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)
Section titled “Branching on a channel stat inside $(eval)”$(plus [FIELD]), $(followers), $(subs), $(subpoints), $(goal METRIC [FIELD]), $(subathon [FIELD]), $(tipgoal [FIELD]) and $(charity [FIELD]) 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 number except $(plus share), a goal’s default output and its text fields (label, next, title, name, charity, source). Money is a plain decimal, like 57.5.
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
Section titled “$(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
Section titled “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 no token in the list above provides it, 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 | — |