Elpho LogoElpho

ElphoScript

Build small automations for your Discord server using a safe, minimal JavaScript subset. Everything runs in a sandbox with explicit modules and predictable control flow.

Runtime v2 Last updated Dec 2025

Public docs: you can copy/paste examples as-is. Quick tip: use("db") enables the module and exposes the globals db / http / embed etc. (don’t assign the result of use() to a variable).

Overview

Scripts run via !parser for quick tests or saved as custom commands. Entry point is alwaysfunction main() { ... }. Load only the modules you need with use().

Think of use("http") as “enable the safe HTTP tools”. After that you can call http.get,http.json, etc.

Runtime rules

  • Entry point: function main() { ... } (required).
  • Import modules explicitly at the top: use("http", "time"), use("db").
  • No arrow functions; helpers as function helper() { } and pass by name.
  • Types: number, string, boolean, array, plain object. No classes.
  • Control flow: if, switch, while, do..while, for, break, continue.
  • ctx is read-only; never mutate it. Reply with ctx.reply.
  • Replies: string, embed().toJSON(), or { type: "json", data }.

Globals & modules

ctx invocation snapshot (user, member, channel, guild, message, command, reply).

use("name") available: db, fetch, http, embed, math, string, time, util, collections, events, scheduler.

db

In-memory key-value store

use("db") db.get/set/delete

Note: db is not a database. It resets on restarts.

http / fetch

HTTP helpers

use("http") http.get/post/json/form, use("fetch") fetch(url, options?) status/text/json

Keep responses small; timeouts are short (~5s).

embed()

Embed builder

setTitle, setDescription, setColor, addField, toJSON

math / string / time / util / collections

random, floor/ceil, clamp, mapRange; lower/upper/replace; now/iso/relative; range/pick/shuffle; groupBy/partition/compact.

events

events.onMessage(handler) for parser testing (named functions only).

scheduler

setTimeout / setInterval with named callbacks; clearTimeout / clearInterval.

Usage patterns

  • Load modules first: use("http", "time").
  • Define helpers above main, especially for events/scheduler.
  • Multiple ctx.reply calls are allowed.
  • Avoid mutating ctx; copy values before changing them.

Minimal examples

javascript
use('http', 'time');
function main() {
const start = time.now();
const res = http.get('https://httpbin.org/status/200');
ctx.reply('status=' + res.status + ' in ' + time.diff(start, time.now()) + 'ms');
}
javascript
use("scheduler");
function onTimeout() {
ctx.reply("Ping!");
}
function main() {
ctx.reply("Ping in 1s");
scheduler.setTimeout(onTimeout, 1000);
}
javascript
use("embed");
function main() {
const card = embed();
card.setTitle("Hello");
card.setDescription("From ElphoScript");
ctx.reply(card.toJSON());
}
javascript
use("collections");
function main() {
const items = [{ ok: true }, { ok: false }];
const parts = collections.partition(items, "ok", true);
ctx.reply("ok=" + parts[0].length + " / not ok=" + parts[1].length);
}

Start here: run your first script

  1. Open Discord and run !parser followed by a triple-code block.
  2. The bot compiles and executes instantly for quick experiments.
  3. When ready, save with !cc create <name> <code>.
bash
!parser ```
function main() {
ctx.reply("Hello, ElphoScript!");
}
```

Tutorial steps (zero → advanced)

Step 1 Replies, variables, math

javascript
use("math");
function main() {
const roll = math.floor(math.random() * 100) + 1;
ctx.reply("You rolled: " + roll);
}

How it works: use("math") enables the safe math helpers. We generate a number withrandom(), floor it, then add 1. ctx.reply sends the string back to Discord.

Step 2 Branches and loops

javascript
function main() {
let hp = 20;
do {
hp = hp - 5;
} while (hp > 10);
for (let i = 0; i < 3; i = i + 1) {
if (i === 1) {
continue;
}
ctx.reply("Ping #" + i);
}
switch (hp) {
case 10:
ctx.reply("Half-life");
break;
default:
ctx.reply("hp=" + hp);
}
}

How it works: do..while decrements HP, the for loop shows continue, andswitch demonstrates branching. All control flow is plain JavaScript supported by the sandbox.

Step 3 Strings, objects, templates

javascript
function main() {
const stats = { loops: 0, status: "pending" };
stats.loops = stats.loops + 1;
const key = "status";
stats[key] = "ok";
ctx.reply("loops=" + stats.loops + ";status=" + stats.status);
}

How it works: plain objects and key lookup—no classes or destructuring. String concatenation is simple and predictable in the runtime.

Step 4 Using Discord context

javascript
function main() {
const args = (ctx.command && ctx.command.args) || [];
const name = args.length > 0 ? args[0] : ctx.user.name;
const mood = args.length > 1 ? args[1] : "happy";
ctx.reply("Hello " + name + "! I see you're " + mood + ".");
}

How it works: ctx.command.args holds command arguments. Ternaries choose defaults fromctx.user. ctx is read-only, so nothing gets mutated.

Step 5 Embeds

javascript
use("embed");
function main() {
const card = embed();
card.setTitle("Profile");
card.setDescription("Snapshot of your account.");
card.addField("User", ctx.user.name);
card.addField("Channel", ctx.channel ? ctx.channel.name : "DM");
card.setColor("#5865F2");
ctx.reply(card.toJSON());
}

How it works: embed() returns a builder. We set fields, then toJSON() returns the structured payload Discord renders as an embed—no manual object crafting.

Step 6 Remembering data with db

javascript
use("db");
function main() {
const key = "user:" + ctx.user.id + ":streak";
let streak = db.get(key) || 0;
streak = streak + 1;
db.set(key, streak);
ctx.reply("Day streak: " + streak);
}

How it works: an in-memory key-value store. The key combines user ID and a name. No JSON stringify/parse— the runtime stores simple types directly.

Step 7 HTTP APIs

javascript
use("http");
function main() {
const res = http.json("https://api.breakingbadquotes.xyz/v1/quotes");
const quote = res && res.json && res.json[0] ? res.json[0] : null;
const text = quote ? '"' + quote.quote + '" ' + quote.author : res.text;
ctx.reply(text || "(No content)");
}

How it works: http.json returns status, text, and parsed json. Careful null checks protect against missing fields. HTTP duration is limited by the sandbox.

Step 8 Events

javascript
use("events");
let invokerId = null;
function onMessage(ev) {
if (!ev || !ev.user || !ev.message) return;
if (invokerId && ev.user.id !== invokerId) return;
ctx.reply("I hear you: " + ev.message.content);
}
function main() {
invokerId = ctx.user.id;
events.onMessage(onMessage);
ctx.reply("Listening for your messages...");
}

How it works: We register a named handler via events.onMessage. The runtime calls it when the parser receives a message. We use a simple invokerId filter so only your messages trigger replies.

Step 9 Combine everything

javascript
use("db", "http", "embed");
function main() {
const counterKey = "quote-runs";
let runs = db.get(counterKey) || 0;
runs = runs + 1;
db.set(counterKey, runs);
const res = http.json("https://api.breakingbadquotes.xyz/v1/quotes");
const quote = res && res.json && res.json[0] ? res.json[0] : null;
const card = embed();
card.setTitle("Quote Run #" + runs);
card.addField("Status", res ? "" + res.status : "???");
card.addField("Quote", quote ? '"' + quote.quote + '" ' + quote.author : res.text || "(no data)");
ctx.reply(card.toJSON());
}

How it works: Count runs in db, fetch JSON with http, build an embed. Pattern: state → I/O → presentation. Each module is explicitly enabled via use().

Function reference (cheat sheet)

ctx

Invocation context

ctx.user, ctx.guild, ctx.channel, ctx.message, ctx.command, ctx.reply(value)

db

Key-value store

db.get, db.set, db.delete

math

Deterministic math

random, floor, ceil, round, clamp, mapRange

string

Text helpers

lower, upper, length, includes, replace

embed()

Embed builder

setTitle, setDescription, setColor, addField, toJSON

http / fetch

Proxy-backed HTTP

get, json, post, headers; fetch(url) for status/text/json

Mini recipes

javascript
use("math", "embed");
function main() {
const dice = 4;
const faces = 6;
let rolls = [];
let total = 0;
for (let i = 0; i < dice; i = i + 1) {
const value = math.floor(math.random() * faces) + 1;
rolls.push(value);
total = total + value;
}
const card = embed();
card.setTitle("Dice results");
card.addField("Breakdown", rolls.join(", "));
card.addField("Total", "" + total);
ctx.reply(card.toJSON());
}
javascript
use("db");
function main() {
const key = "user:" + ctx.user.id + ":reminders";
const reminders = db.get(key) || [];
reminders.push("Finish docs");
db.set(key, reminders);
ctx.reply("Saved reminders: " + reminders.length);
}
javascript
use("http", "embed");
function main() {
const res = http.json("https://api.mcstatus.io/v2/status/java/play.hypixel.net");
const body = res && res.json ? res.json : null;
const players = body && body.players && body.players.online ? body.players.online : 0;
const status = body && body.online ? "Online" : "Offline";
const card = embed();
card.setTitle("Hypixel status");
card.addField("State", status);
card.addField("Players", "" + players);
card.setColor(status === "Online" ? "#22c55e" : "#ef4444");
ctx.reply(card.toJSON());
}

Interactive build guides

Quick poll command

javascript
use("embed");
function main() {
const args = ctx.command && ctx.command.args ? ctx.command.args : [];
if (args.length < 2) {
ctx.reply("Usage: !poll <choice1> <choice2> [choice3] [choice4] [choice5]");
return;
}
const card = embed();
card.setTitle("Quick Poll");
for (let i = 0; i < args.length && i < 5; i = i + 1) {
card.addField("Option " + (i + 1), args[i]);
}
ctx.reply(card.toJSON());
}

Focus reminder board

javascript
use("db", "embed");
function main() {
const key = "user:" + ctx.user.id + ":focus";
const notes = db.get(key) || [];
const text = ctx.command && ctx.command.args && ctx.command.args.length > 0 ? ctx.command.args.join(" ") : null;
if (text) {
notes.push(text);
}
while (notes.length > 5) {
notes.shift();
}
db.set(key, notes);
const card = embed();
card.setTitle("Focus reminders");
card.addField("Total saved", "" + notes.length);
card.addField("Latest", notes.length > 0 ? notes[notes.length - 1] : "(none)");
ctx.reply(card.toJSON());
}

Troubleshooting

  • Unknown identifier: only globals listed here exist; declare with let/const.
  • Unsupported syntax: avoid destructuring, spread, classes; keep to basic JS.
  • Non-function call: ensure you call real functions; coerce with "" + value instead of constructors.
  • Fetch timed out: the proxy times out around 5s; retry or pick a faster endpoint.

Practice ideas

  • Dice cup: roll n dice, sum them, render an embed breakdown.
  • Reminder list: store per-user reminders in db and list them.
  • MC status card: call MCStatus, show player counts, color the embed green/red.
  • Poll: accept up to five options, add fields, and guide users to react.