Skip to content

AI token DSL reference

Token syntax overview

AI command response templates support the same $(…) token syntax as Chatters commands. The following tokens are available:

TokenDescription
$(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 arguments

NAME 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

ConstructExamples
Literals42, "hello", 'world', true, false, null
Operators`+ - * / % == != < > <= >= &&
Ternarycond ? a : b (chain by nesting in the alt branch)
Let bindingslet NAME = EXPR; EXPR (chain with ;)
Built-in functionsrandom(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)
MathMath.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 accessat(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) — 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) 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 a 0 — a 0 would 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.