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 !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. 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:

EventCategoryFires when
on message("!trigger")MessagesSomeone sends a message starting with !trigger. Exposes message and user.
on member_join()Server EventsA new member joins the server. Exposes member.
on member_leave()Server EventsA member leaves (or is removed from) the server. Exposes member.
on reaction_add()ReactionsSomeone 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:

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.")

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

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.

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

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)
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.

CommandWhat it does
!pingReplies with the bot's current latency, useful for checking it is online and responsive.
!infoReplies with how many custom commands are currently enabled in the server.
!configReplies 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:

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 (or, for events like member_join, the handler needs to be a zero-argument on member_join())
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.