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
!greetor!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
endis the code that runs send(...)posts a message back to the channel — see Built-in Functions belowendcloses the handler (everyon,if,while,for, anddefneeds a matchingend)
3. Values and Variables
Lumen has six kinds of values:
| Type | Example | Notes |
|---|---|---|
Number | 42, 3.14 | Whole numbers and decimals both work |
String | "hello" | Text, always in double quotes |
Boolean | true, false | Used in conditions |
Nil | nil | Represents “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
| Category | Operators | Example |
|---|---|---|
| Arithmetic | + - * / % | score + 10 |
| Comparison | == != < > <= >= | score >= 100 |
| Logical | and or not | is_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
| Function | What it does | Example |
|---|---|---|
length(x) | Length of a string, list, or dict | length("hello") → 5 |
keys(d) | List of a dict's keys | keys({"a": 1}) → ["a"] |
upper(s) | Uppercase a string | upper("hi") → "HI" |
lower(s) | Lowercase a string | lower("HI") → "hi" |
str(x) | Convert anything to text | str(42) → "42" |
int(x) | Convert text/number to a whole number | int("7") → 7 |
float(x) | Convert text/number to a decimal | float("2.5") → 2.5 |
split(s, sep) | Split a string into a list | split("a,b", ",") → ["a", "b"] |
join(list, sep) | Join a list into one string | join(["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 found | index_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:
| Function | What 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:
| Message | What it means |
|---|---|
Unexpected token ... at line N | A 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 String | You tried to do math with text — check int()/float() |
has no on message("...") handler matching the trigger | The trigger word you set doesn't match anything in the on message(...) line |
Script exceeded ... step limit | Almost 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.