Lists in Overlabels
User-owned arrays of values that streamers manage from the dashboard or chat. Raffles, queues, quote walls, leaderboards, donation goals.
What is a List?
A List is a named array of values you own. Each List has a slug (the identifier you reference in overlays and chat) and an optional human-readable label. The values inside can be anything - viewer names, custom messages, numbers, URLs - and Overlabels stores them exactly as they were entered. No deduplication, no whitespace trimming, no quiet reordering. Lists are lists.
You reference a List in your overlay HTML/CSS with [[[c:list:your_slug]]]. That tag resolves to the
full array as a JSON string when used bare; the more common usage is one of the derived read tags (see
Reading from a List) or a foreach loop.
Lists are user-scoped: every streamer has their own. They are not shared across users. A List you create is only visible inside your overlays, your chat commands, and your dashboard.
Creating and editing Lists
Lists live at /dashboard/lists. Click "New list", pick a slug (lowercase letters, digits, underscores - must start with a letter, max 50 chars), optionally a label, and optionally a starting set of items - one per line.
Why no dashes in the slug? Tag parser context. A slug like my-raffle would collide with hyphen
handling inside the tag namespace. Snake_case keeps everything composable.
Once a List exists you can edit its items in a freeform textarea, one item per line. Empty lines, leading or trailing whitespace, and duplicates are all preserved exactly. The only character we strip is the NUL byte (because it breaks JSON encoding and you didn't actually mean to type it).
Some Lists are created automatically by recipes you install. Those show a "from Recipe" badge and may be locked (the recipe declares whether you can edit the items). Locking only affects items - you can still disable/enable a recipe-managed List from your dashboard.
Reading from a List in your overlay
Every List ships a set of tags into your overlay's data store on render. Here's the full set for a List
with slug donors and items ["Alice", "Bob", "Carol"]:
| Tag | Resolves to |
|---|---|
[[[c:list:donors]]] |
["Alice","Bob","Carol"] (the values as a JSON string) |
[[[c:list:donors:json]]] |
The full item objects as a JSON string. |
[[[c:list:donors:first]]] |
Alice |
[[[c:list:donors:last]]] |
Carol |
[[[c:list:donors:count]]] |
3 |
[[[c:list:donors:empty]]] |
0 (would be 1 if empty) - pair with conditional tags |
[[[c:list:donors:random]]] |
Random item - stable per overlay mount (does not re-roll on each broadcast) |
[[[c:list:donors:sum]]] |
Numeric sum of items. Empties and whitespace are 0; non-numeric content shows an inline error pointing at the offending row. |
[[[c:list:donors:expires_at]]] |
Unix seconds when the List expires (empty when no deadline set). |
[[[c:list:donors:countdown]]] |
Live seconds remaining until expiry. Ticks every frame; pair with formatting pipes. |
All tags update live. When the underlying List changes - because you edited it, because a chat appender fired, because the sweeper aged out an entry - every overlay reading it patches its data store and re-renders. No reload, no polling.
The item data model and :json
Under the hood, a List item is no longer just a bare string - it is a small object with a stable
identity and room for richer data. You don't have to think about this for everyday use: every tag and
foreach on this page keeps working exactly as before, projecting to the item's value. This section is
for when you want to build something richer on top of a List - a custom wheel, an animated leaderboard, a
web component - and need the full data.
Nothing you already built changes. [[[c:list:slug]]], .first, :last, :sum, :count, and
all still resolve to value strings, exactly as they
always have. The object model is purely additive - it adds the new [[[foreach:c:list:slug as item]]][[[item]]]:json tag and gives every item a
stable id, without touching any tag you're already using.
The item shape
Each item carries six fields:
| Field | Meaning |
|---|---|
id |
A whole number assigned by the server, unique within the List, stable for the life of the item, and never reused - even after the item is drawn, popped, or the List is cleared. This is the reliable key for animation: two items with the same value still have different ids. |
value |
The content you typed (or a viewer appended). The string every scalar tag projects to. Always present; may be empty - we never strip your content. |
added_at |
Unix seconds for when the item was added. This is what per-item age-out measures against, and it's handy for "added 3 minutes ago" displays. |
label |
reserved Optional display label. Always null today. |
weight |
reserved Picker weight for weighted draws. Always 1 today. |
color |
reserved Optional hex color (#rgb / #rrggbb). Always null today. |
About the reserved fields. label, weight, and color are part of the data shape and ship in
:json, but there is no way to set them yet - every item is created with label: null,
weight: 1, color: null. They're reserved so the editing UI for them can land later without another
data migration. Build against id, value, and added_at today; treat the rest as defaults until
weighted/colored editing ships.
The :json tag
[[[c:list:donors:json]]] resolves to the full array of item objects as a JSON string. For a List with
items Alice, Bob, Carol:
[
{ "id": 1, "value": "Alice", "added_at": 1730000000, "label": null, "weight": 1, "color": null },
{ "id": 2, "value": "Bob", "added_at": 1730000060, "label": null, "weight": 1, "color": null },
{ "id": 3, "value": "Carol", "added_at": 1730000120, "label": null, "weight": 1, "color": null }
]
It updates live like every other List tag - append, draw, edit, or age-out, and the :json payload
re-renders with the new objects. The bare [[[c:list:slug]]] tag stays an array of value strings for
backward compatibility; :json is the opt-in that gives you the objects.
Important: overlays run no JavaScript. Overlay templates are display-only HTML and CSS - we strip
<script>, inline event handlers, and <iframe> on save. So dropping [[[c:list:slug:json]]] into a
template just prints the JSON as text. Useful for a quick peek at the data; useless for building a
wheel, because there's no way to parse it inside the overlay.
To actually use the rich data, read it from outside the overlay - a small page you host yourself, where JavaScript is allowed. That's what the read endpoint below is for.
Reading a List from outside: the JSON endpoint
Every List is also readable as pure JSON from a public, token-authed endpoint:
GET https://overlabels.com/api/lists/<slug>?token=<your overlay token>
The token is an Overlay Access Token from your dashboard - the same kind your overlay URLs use. It
identifies you, so you only ever read your own Lists (treat it like sharing an overlay URL: anyone who
has it can read that List). The response is the full item objects plus a little metadata:
{
"slug": "wheel",
"label": null,
"count": 2,
"items": [
{ "id": 1, "value": "Pizza", "added_at": 1730000000, "label": null, "weight": 1, "color": null },
{ "id": 2, "value": "Tacos", "added_at": 1730000060, "label": null, "weight": 1, "color": null }
],
"disabled_at": null, "expires_at": null, "entry_ttl_seconds": null,
"updated_at": 1730000060, "ts": 1730000061
}
It's read-only and cross-origin (CORS-open), so a browser page on any host can fetch it. Note this does not break the "overlays never phone home" rule - the overlay isn't calling anything (it can't, no JS); a separate page you control is, and only to read.
Live updates (no polling)
The response also carries a realtime block - everything your page needs to subscribe to live
changes instead of polling:
"realtime": {
"channel": "lists.<your twitch id>.wheel",
"event": "list.updated",
"auth_endpoint": "https://overlabels.com/api/overlay/broadcasting/auth",
"key": "...", "host": "...", "port": 443, "scheme": "https"
}
Point a Pusher/Echo client at key/host/port, authorize the channel by POSTing your token to
auth_endpoint (the same token gates both the read and the subscribe), and every chat append, draw,
edit, or age-out pushes the new state to your page instantly - a chatter types !raffle and your wheel
grows a slice in real time. The copy-paste consumer at docs/examples/list-data-consumer.html does the
full bootstrap-then-subscribe dance.
Want the full walkthrough? The Lists in realtime guide takes you end to end: get a token, read the list, render it, subscribe to live updates, and add your page to OBS - with a troubleshooting table for when it doesn't work the first time.
Why this matters: stable identity
The single biggest reason items became objects is the id. A wheel, a leaderboard, or any animated
consumer needs to answer "is this the same item I drew last frame, or a new one that happens to have the
same name?" With bare strings you couldn't - two viewers both named guest were indistinguishable. With
ids, you key your DOM and your animations off item.id and the ambiguity is gone at the root.
Worked example: feeding a custom wheel
The wheel is a small page you host yourself (GitHub Pages, tiiny.host, anywhere) and add to OBS as its own Browser Source - separate from your Overlabels overlay. It fetches the endpoint and renders however you like, in plain JavaScript:
<div id="wheel"></div>
<script>
const TOKEN = '...'; // an Overlay Access Token from your dashboard
const URL = `https://overlabels.com/api/lists/wheel?token=${TOKEN}`;
async function load() {
const res = await fetch(URL);
const { items } = await res.json();
// items: [{ id, value, added_at, label, weight, color }, ...]
// Key each slice by item.id so two identical names never collide,
// and so an item that survives a draw keeps its slice.
document.getElementById('wheel').replaceChildren(...items.map(it => {
const slice = document.createElement('div');
slice.dataset.id = it.id;
slice.textContent = it.value;
return slice;
}));
}
load();
// Fetch once, or poll for now: setInterval(load, 5000)
</script>
That's the whole pattern: fetch the endpoint, objects out, render however you like, key by id. A
copy-paste-ready version of this page lives in the repo at docs/examples/list-data-consumer.html.
Once weighted and colored editing lands, the same page reads it.weight and it.color with no other
change - that's exactly why those fields are already in the shape.
Iterating with foreach
The derived tags above are great for "show the first item" or "show the count". When you want to render
every item, use foreach:
<ul>
[[[foreach:c:list:donors as donor]]]
<li>[[[donor]]]</li>
[[[endforeach]]]
</ul>
Inside the loop, [[[donor]]] resolves to each item's value in turn. The loop body can use any other
tag the overlay knows about, and the template engine materialises one block per item.
Per-item field access in a loop is not available yet. [[[donor]]] gives you the value;
[[[donor.color]]] and friends are not wired up yet (it needs a change to how foreach
materialises items). If you need each item's id or added_at today, read :json and iterate it in
your own script instead.
See the Reference page for the full foreach syntax, including index access and
nested iteration.
Chat appenders - viewers grow your List
A chat appender wires a custom command (like !raffle or !join) to a List. When a viewer runs the
command, the appender resolves a template string and pushes the result into the target List. The viewer's
name lands in the array; the overlay updates live.
Each appender configures:
- Command - the bang word viewers type (must be unique across your custom commands, recipes, and meta-commands)
- Target List - which List the appended value goes to
- Permission level - everyone, follower, subscriber, vip, moderator, broadcaster
- Cooldown - global cooldown in seconds (broadcaster bypasses)
- Value template - the string to append. Uses the same template language as
Bot Commands:
[[[bot:from_user]]],[[[bot:args]]], control reads, pipe formatters - Empty-args reply - what to say when the chatter forgot to type an argument the template expects
- Dedup policy -
none,per_chatter, orper_chatter_per_stream - Max size - hard cap; further fires silently refuse (so slot 100 actually means slot 100)
Example - raffle entry by display name, one per chatter per stream:
| Setting | Value |
|---|---|
| Command | raffle |
| Target List | raffle_entries |
| Value template | [[[bot:from_user]]] |
| Dedup | per_chatter_per_stream |
Example - quote wall, value is whatever the viewer typed: command quote, value template
, empty-args reply
[[[bot:from_user]]]: [[[bot:args]]]@[[[bot:from_user]]] you forgot the quote! Use !quote whatever the streamer said.
The !list meta-command
!list is a single configurable chat command that gives you (and your mods) the full action vocabulary
against any of your Lists from chat, without wiring a separate command per action. You opt in once from
/dashboard/lists and pick the command name - default is !list, but if that collides
with a bot you already use (StreamElements, Nightbot, Fossabot), rename it to whatever you like (!ol,
!l, !mylist...).
Syntax is always:
!list <slug> <action> [args...]
Examples:
!list raffle_entries count
!list raffle_entries draw
!list raffle_entries clear
!list raffle_entries pop first
!list raffle_entries clone backup_today
!list quotes random 3
!list quotes last
!list shoutouts disable
Permission level is fixed to moderator and above. The action vocabulary is destructive or chat-emitting, and we don't want a random chatter clearing your raffle list. Broadcaster and moderators can run it; everyone else's invocations are ignored silently.
The command is self-documenting. Bare !list replies with global help. !list <slug> with no action
replies with the per-list help. An unknown action lists the valid ones. Missing required arguments (like
!list raffle pop with no first|last) reply with usage hints inline rather than failing silently.
The action vocabulary in detail
The same ten verbs are available from both the chat !list command and the action buttons on
/dashboard/lists. They split into three groups by semantics.
Read actions (no mutation, no snapshot)
| Action | What it does |
|---|---|
count |
Replies with the number of items. |
first [N] |
Replies with the first N items (default 1, max = list size). |
last [N] |
Replies with the last N items (default 1, max = list size). |
random [N] |
Replies with N random items, without replacement (default 1). |
Destructive actions (auto-snapshot, broadcast)
| Action | What it does |
|---|---|
draw |
Picks a random item, removes it, announces the winner. The classic raffle action. |
clear |
Empties the List. Use before starting a fresh raffle or queue. |
pop first |
Removes and announces the head of the List. Useful for FIFO queues - "who's next?". |
pop last |
Removes and announces the tail of the List. |
clone <new_slug> |
Duplicates the List into a new List with the given slug. Inherits items, label, and item ages verbatim. |
Every destructive action automatically creates a snapshot of the List's previous state before it mutates. That snapshot is what makes undo possible.
State actions
| Action | What it does |
|---|---|
disable |
Disables the List. Chat appenders silently refuse; existing items stay visible. |
enable |
Re-enables a disabled List. |
Snapshots - undo for destructive actions
Every time a destructive action runs - clear, draw, pop, or a restore from an earlier snapshot -
Overlabels writes the List's previous state into a snapshot row. You can also take manual snapshots from
the dashboard before a risky edit.
The snapshots panel under each List shows up to 50 recent snapshots with their reason badge, item count, and age. You can:
- Restore - replace current items with the snapshot's items. Creates a
before_restoresnapshot first, so the restore is itself undoable. - Pin - exempt the snapshot from auto-retention. Pinned snapshots stay until you unpin or delete them.
- Delete - remove a single snapshot immediately. No further undo.
- Save snapshot - take a manual snapshot of the current state right now.
Retention. Unpinned snapshots are automatically deleted 30 days after they were created and cannot be recovered after that point. Pinned snapshots stay until you act on them. Deleting a List removes all its snapshots, pinned or not. This retention behavior is also covered in the privacy policy.
Auto-expiry - entry age-out and whole-list deadlines
Two independent timers you can set on a List from the dashboard's Expiry panel:
Per-item age-out
Set Per-item age-out (seconds, minutes, or hours, max 30 days) and any item older than that is removed automatically. The sweeper runs every minute. Useful for rolling shoutout walls, "recent donors" displays, queue cleanups, anything where staleness is bad.
Reordering items in the dashboard preserves their age. Renaming an item (or typing a new one) resets that entry's age to zero. Cloning a List inherits item ages verbatim, so a mid-stream clone of a 5-minute-old raffle entry keeps the 5 minutes already accrued.
Whole-list deadline
Set Whole-list deadline to a future moment, and at that moment the List is snapshotted, cleared, and disabled. The snapshot follows the regular 30-day retention rule. Clearing the deadline on a previously-expired List also re-enables it - "reopen" is a single action.
Tags for the deadline
Two tags surface the deadline directly in your overlay:
| Tag | What it resolves to |
|---|---|
[[[c:list:raffle:expires_at]]] |
Unix seconds of the deadline. Empty string when no deadline is set. |
[[[c:list:raffle:countdown]]] |
Seconds remaining (clamped at zero), updated every frame. Pair with a duration formatter for display. |
Example - live mm:ss countdown in your overlay:
Raffle closes in [[[c:list:raffle:countdown|duration:mm:ss]]]
See formatting pipes for the full duration pattern reference (hh:mm:ss,
dd:hh:mm:ss, etc).
Disable and enable
Disabling a List flips a single flag with two visible effects:
- Chat appenders silently refuse new appends. No error message, no apology - you disabled it intentionally.
- Existing items stay visible to overlays. You can still curate them manually from the dashboard.
Use it when a raffle has closed but you're not ready to draw yet, when a queue is paused for a break, or
when you want to freeze a state for screenshotting without losing chat-appender wiring. Toggle from the
dashboard, from chat via !list <slug> disable, or implicitly through whole-list expiry.
Worked examples
Raffle - !raffle to enter, !list raffle draw to pick a winner
- Create a List
raffle_entries. - Create a chat appender: command
raffle, targetraffle_entries, value template[[[bot:from_user]]], dedupper_chatter_per_stream. - Opt into the
!listmeta-command. - Optionally set a whole-list deadline so the raffle closes on its own. Put
[[[c:list:raffle_entries:countdown|duration:mm:ss]]]in your overlay. - When the deadline hits, the List disables itself. Run
!list raffle_entries drawfrom chat to pick the winner. - Want to redraw? The before-draw snapshot is right there in the snapshots panel - restore and
drawagain.
FIFO queue - !join to enter, !list queue pop first for next up
-
Create a List
queue. -
Create a chat appender: command
join, value template[[[bot:from_user]]], dedupper_chatter(lifetime, not per-stream). -
Put the queue in your overlay as a foreach:
[[[foreach:c:list:queue as player]]]<li>[[[player]]]</li>[[[endforeach]]] -
Each play, run
!list queue pop firstin chat to grab the next person.
Quote wall - !quote to add, random rotation in overlay
- Create a List
quotes. - Create a chat appender: command
quote, value template, permission[[[bot:from_user]]]:[[[bot:args]]]moderator. - In your overlay use
[[[c:list:quotes:random]]]to show one quote at a time. It stays stable per overlay mount; reload the browser source to pick a new one. - Per-item age-out optional - set 30 days to keep the wall recent without manual pruning.
Donation tally - List of amounts, :sum as the goal driver
When you don't want to wire a full Ko-fi or StreamLabs integration but you do want a quick tally:
-
Create a List
tips. -
Create a chat appender: command
tip, value template[[[bot:args]]], permissionmoderator(so only mods log tips after confirming them out-of-band). -
In your overlay:
Raised: €[[[c:list:tips:sum]]]of €500 goal -
Want a progress bar? Create an Expression Control
tip_pctwith expressionc.list.tips.sum / 500 * 100and reference[[[c:tip_pct]]].
Things to know
Lists are lists. We don't sanitise content.
Whatever you (or your viewers) put in, we keep. Empty lines, duplicates, 200x the same value, lengthy whitespace - it's all yours. The only character stripped is the NUL byte, because it would break JSON serialisation downstream. This contract is intentional: opinionated trimming would surprise people who actually want what they typed.
:random is stable per overlay mount.
The random tag picks once on initial render and keeps the same value across broadcasts. Otherwise every
append would re-roll it and your overlay would flicker. To pick a new random item, reload the browser
source. To pick at the moment of an action, use the chat !list <slug> random action instead.
Mod permission is the floor for !list.
The meta-command can clear, draw, disable, and clone. We don't want a viewer running !list raffle clear
mid-raffle. Chat appenders (!join, !raffle) have their own independent permission setting that can be
looser (everyone, follower, subscriber, etc).
Command-name collisions are checked at save time.
A chat appender's command, the !list meta-command, your Bot Commands, your recipe triggers, and the
built-in commands all share the same namespace. Save-time validation refuses collisions with a clear
error rather than silently letting one of them win at runtime.
Sweeps run every minute.
Both per-item age-out and whole-list deadlines are evaluated by a sweep that runs every minute. So a 30-second TTL doesn't mean entries vanish on the second; they vanish on the next sweep tick after they age past the cutoff. This is fine for "trim things that should not stick around" - if you need exact timing, use a timer Control instead.
Recipe-managed Lists.
Recipes can create Lists on your behalf. They show up with a "from Recipe" badge. If the recipe declared the items locked, you can't edit them - uninstall the recipe to free the List. You can still disable, enable, set TTLs, and use the action vocabulary against locked Lists.
Live updates everywhere.
Every change to a List (manual edit, chat append, action, sweeper) broadcasts to your overlays and your dashboard. Overlays patch their data store in place. The dashboard page patches the active row; if you're mid-edit with unsaved changes, your draft wins until you save - we don't trash your work.
Quick reference card
Tags
[[[c:list:slug]]] JSON array of value strings
[[[c:list:slug:json]]] JSON array of item objects {id,value,added_at,label,weight,color}
[[[c:list:slug:first]]] first item
[[[c:list:slug:last]]] last item
[[[c:list:slug:count]]] item count
[[[c:list:slug:empty]]] "1" if empty, "0" otherwise
[[[c:list:slug:random]]] random item (stable per mount)
[[[c:list:slug:sum]]] numeric sum of items
[[[c:list:slug:expires_at]]] deadline as Unix seconds
[[[c:list:slug:countdown]]] live seconds remaining
Foreach
[[[foreach:c:list:slug as item]]] [[[item]]] [[[endforeach]]]
Chat - !list meta-command (mod+)
!list <slug> count
!list <slug> first [N]
!list <slug> last [N]
!list <slug> random [N]
!list <slug> draw
!list <slug> clear
!list <slug> pop first|last
!list <slug> clone <new_slug>
!list <slug> disable
!list <slug> enable
In an Expression Control
c.list.slug.sum
c.list.slug.count
For deeper context on the template language and pipe formatters, see Conditional and Event Tags and Formatting Pipes. For numeric computation on top of Lists, see Expression Controls.