Lumen Language Guide
A scripting language for Discord commands. This page covers everything you need to write your own, no programming experience required.
Download this guide 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. The Two Editors: Code and Blocks
Every command's editor has two modes, switchable with the Code / Blocks toggle above the editor:
- Code is a plain text editor with syntax highlighting and live error checking. This is the mode this guide is written for, and it can express anything the Lumen language supports.
- Blocks is a drag-and-drop node canvas for people who would rather not type code. You drop nodes like "Say something" or "Only reply sometimes" onto a canvas and connect them in order with wires, starting from the Trigger node. See the Reference page for the full list of available nodes.
Both modes produce the exact same underlying Lumen source, saved the same way. Switching to Blocks reads your existing code and tries to rebuild it as nodes; if the code uses something the node set does not understand (loops, custom functions, or anything more advanced), you will see a message saying so, and the Code view is left untouched. Anything built in Blocks can always be viewed and fine-tuned in Code afterward.
4. Events
Every command starts with on <event>(...). Which event you pick decides when the code inside runs. Events are grouped into categories in the "New command" dropdown on your server's dashboard:
| Event | Category | Fires when |
|---|---|---|
on message("!trigger") | Messages | Someone sends a message starting with !trigger. Exposes message and user. |
on member_join() | Server Events | A new member joins the server. Exposes member. |
on member_leave() | Server Events | A member leaves (or is removed from) the server. Exposes member. |
on reaction_add() | Reactions | Someone adds a reaction to a message. Exposes user and reaction. |
member_join, member_leave, and reaction_add don't take trigger text in the parentheses: they fire on their own, so you can only have one of each per server. message commands each need their own unique trigger word, so you can have as many of those as you like.
on member_join()
send("Welcome to the server, {member.name}!")
end
5. 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.")
Keep it to a plain variable or function call inside { }: avoid putting a quoted piece of text directly inside the braces. If you need something like join(list, ", ") inside a message, compute it into a variable on its own line first, then interpolate the variable:
tags = ["fun", "music"]
joined = join(tags, ", ")
send("Tags: {joined}")
6. 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.
7. 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.
8. 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.
9. 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
10. 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) |
react(emoji) | Reacts to the triggering message with an emoji (or, on a reaction event, the message that was reacted to) |
Context values (message, user, member, reaction)
Depending on which event a handler is for, it gets access to different values describing what triggered it (see the Events section above for which event exposes which):
on message("!whoami")
send("You are {user.name}, and you said: {message.content}")
end
on member_join()
send("Welcome, {member.name}!")
end
on reaction_add()
send("{user.name} reacted with {reaction.emoji}")
end
11. 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)
joined = join(tags, ", ")
send("Tags are now: {joined}")
end
end
12. Built-in Bot Commands
A few commands are always available in every server the bot is in, without you having to write any Lumen for them. They cannot be renamed or overridden: a script whose trigger matches one of these gets rejected when you try to save it.
| Command | What it does |
|---|---|
!ping | Replies with the bot's current latency, useful for checking it is online and responsive. |
!info | Replies with how many custom commands are currently enabled in the server. |
!config | Replies with a direct link to this server's command dashboard on the website. |
From your server's Settings page, you can change the prefix these three commands respond to (the default is !), and turn off any of them individually if you would rather your own commands used that name instead. Turning one off just means the bot stops responding to it, your own custom commands are never affected by these settings.
13. 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 (or, for events like member_join, the handler needs to be a zero-argument on member_join()) |
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.