Lumen Language Guide

A scripting language for Discord commands. This page covers everything you need to write your own — no programming experience required.

Download as a Word document (.docx)

1. Introduction

Lumen is the scripting language behind your server's custom bot commands. Every command you build in the website's editor is written in Lumen and saved as a .lum script.

Lumen was built to be easy to read even if you've never programmed before, while still being safe to run: every script is limited in how long it can run and what it's allowed to touch, so a mistake in one command can never take down the whole bot or affect anyone else's server.

What you can build

  • Custom chat commands that reply to a trigger word, like !greet or !roll
  • Commands that remember information across a conversation using variables and dicts
  • Commands that make decisions (if a user is an admin, if a number is high enough, etc.)
  • Reusable helper functions you can call from multiple commands

2. Getting Started

Every Lumen script lives inside an event handler. The simplest possible command looks like this:

on message("!hello")
    send("Hello there!")
end

This says: when someone sends a message that starts with !hello, run send("Hello there!"), which posts that text back to the channel.

Anatomy of a command

  • on message("...") — starts a handler; the text in quotes is the trigger word people type
  • Everything between the handler line and end is the code that runs
  • send(...) posts a message back to the channel — see Built-in Functions below
  • end closes the handler (every on, if, while, for, and def needs a matching end)

3. Values and Variables

Lumen has six kinds of values:

TypeExampleNotes
Number42, 3.14Whole numbers and decimals both work
String"hello"Text, always in double quotes
Booleantrue, falseUsed in conditions
NilnilRepresents “nothing”
List[1, 2, 3]An ordered collection
Dict{"name": "Alice"}Key/value pairs, keys are always text

Variables are created just by assigning to them — no special keyword needed:

name = "Alice"
age = 30
scores = [10, 20, 30]
profile = {"name": "Alice", role: "admin"}

Dict keys can be written as plain words (role: "admin") or as quoted text ("name": "Alice") — both work the same way. => also works instead of :.

String interpolation

Drop a value straight into a string using curly braces:

name = "Alice"
send("Hello, {name}! You have {length(name)} letters in your name.")

4. Operators

CategoryOperatorsExample
Arithmetic+ - * / %score + 10
Comparison== != < > <= >=score >= 100
Logicaland or notis_admin and score > 0
Assignment= += -=score += 5

The + operator also joins strings and lists together, and every value except false and nil counts as true in a condition — including 0 and an empty string, which is different from some other languages.

5. Control Flow

if / elsif / else

if score > 100
    send("High score!")
elsif score > 10
    send("Not bad.")
else
    send("Keep trying.")
end

while

count = 0
while count < 5
    count += 1
    send("Count is {count}")
end

for

for item in ["sword", "shield", "potion"]
    send("You have a {item}")
end

Both loop types support break (stop the loop early) and continue (skip to the next iteration). Loops are protected by a safety limit — see Safety and Limits below — so an accidental infinite loop stops itself instead of running forever.

6. Safety and Limits

Every script runs inside a sandbox. This isn't a restriction to work around — it's what lets the bot safely run scripts written by different people across different servers without any one script affecting another.

  • A script can only call functions it defines itself, the built-in functions listed below, and specific actions the bot exposes (like send) — nothing else. There is no way for a script to read files, access the network, or affect anything outside its own server.
  • Every script has a step limit. If a loop runs too long (usually a sign of an accidental infinite loop), the script stops itself with a clear error instead of hanging.
  • Every script has a time limit (a couple of seconds). A script that takes too long is stopped automatically.
  • A mistake in one command — a typo, a missing variable, bad logic — only ever fails that one command. It cannot crash the bot or affect other commands or other servers.

7. Functions

Group reusable logic into a function with def, and use it from any command:

def shout(text)
    return text + "!!!"
end

on message("!hype")
    send(shout("let's go"))
end

8. Built-in Functions

Always available

FunctionWhat it doesExample
length(x)Length of a string, list, or dictlength("hello") → 5
keys(d)List of a dict's keyskeys({"a": 1})["a"]
upper(s)Uppercase a stringupper("hi") → "HI"
lower(s)Lowercase a stringlower("HI") → "hi"
str(x)Convert anything to textstr(42) → "42"
int(x)Convert text/number to a whole numberint("7") → 7
float(x)Convert text/number to a decimalfloat("2.5") → 2.5
split(s, sep)Split a string into a listsplit("a,b", ",")["a", "b"]
join(list, sep)Join a list into one stringjoin(["a", "b"], "-") → "a-b"
contains?(x, v)Does a string/list/dict contain v?contains?([1,2], 2) → true
index_of(x, v)Position of v, or nil if not foundindex_of("hi", "i") → 1
push(list, v)Add v to the end of a list (in place)push(list, 3)
pop(list)Remove and return the last item (in place)pop(list) → 3

A note on push and pop: unlike every other built-in function, these change the list you pass in directly rather than giving you back a new one. If two variables point at the same list, changing it through one shows up through the other too — this is the one place in Lumen where that matters.

Provided by the bot

These come from the bot itself, so exactly which ones exist can grow over time. As of this guide:

FunctionWhat it does
send(text)Posts a message to the channel the triggering message came from
random(min, max)A random whole number between min and max, inclusive
wait(seconds)Pauses briefly before continuing (capped at a few seconds)

message and user

Every command has access to two special values describing what triggered it:

on message("!whoami")
    send("You are {user.name}, and you said: {message.content}")
end

9. Example Commands

A dice roller

on message("!roll")
    n = random(1, 6)
    send("You rolled a {n}")
end

A greeting that checks who's talking

on message("!greet")
    if user.is_admin
        send("Welcome back, boss.")
    else
        send("Hi, {user.name}!")
    end
end

Looking things up in a dict

on message("!classinfo")
    classes = {
        warrior: "High health, melee damage",
        mage: "Low health, powerful spells",
        rogue: "Fast, high critical chance"
    }
    for name in keys(classes)
        send("{upper(name)}: {classes[name]}")
    end
end

Splitting a message into arguments

Trigger words only match the first word of a message, so the rest is up to you to split apart:

on message("!addtag")
    words = split(message.content, " ")
    tag = words[1]
    tags = ["fun", "music"]
    if contains?(tags, tag)
        send("{tag} is already a tag.")
    else
        push(tags, tag)
        send("Tags are now: {join(tags, ", ")}")
    end
end

10. Common Errors

The editor checks your script before saving it, so most mistakes are caught immediately with a clear message:

MessageWhat it means
Unexpected token ... at line NA typo or missing end / quote / bracket near that line
Undefined variable 'x'You used a variable before assigning it a value
Expected a number, got StringYou tried to do math with text — check int()/float()
has no on message("...") handler matching the triggerThe trigger word you set doesn't match anything in the on message(...) line
Script exceeded ... step limitAlmost always an infinite while loop — check the loop's condition

If a script fails after it's already saved (a runtime error rather than a typo), only that one command fails — everything else keeps working.