Skip to content

AI token DSL reference

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.

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 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 arguments

NAME 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.

// 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"}

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).

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.

  • var, const, function, =>, return, if, while, for, try
  • Bare assignment (x = 5) — use let
  • Bracket indexing (args[0]) — use at(args, 0)
  • Invented identifiers — only user, touser, channel, query, args exist
  • 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

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 a 0 — a 0 would make the “you need N more points” branch state a fault as a fact about the month.
// 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"}

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.