ElphoScript
Build automations in a safe, minimal JavaScript subset. Everything runs in a sandbox with explicit modules and predictable control flow.
Runtime v2 Last updated Dec 2025
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().
Runtime rules
- Entry point:
function main() { ... }(required). - Import modules explicitly at the top:
use("module", ...). - 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.
ctxis read-only; never mutate it. Reply withctx.reply.- Replies: string,
embed().toJSON(), or{ type: "json", data }.
Globals & modules
ctx invocation snapshot (user, member, channel, guild, message, command, reply).
use(...modules) available: db, fetch, http, embed, math, string, time, util, collections, events, scheduler.
db
In-memory key-value store
db.get/set/delete
http / fetch
HTTP helpers
http.get/post/json/form, fetch(url, options?) status/text/json
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 dev parser listeners (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.replycalls are allowed. - Avoid mutating
ctx; copy values before changing them.
Minimal examples
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");}use("scheduler");
function onTimeout() { ctx.reply("Ping!");}
function main() { ctx.reply("Ping in 1s"); scheduler.setTimeout(onTimeout, 1000);}use("embed");
function main() { const card = embed(); card.setTitle("Hello"); card.setDescription("From ElphoScript"); ctx.reply(card.toJSON());}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
- Open Discord and run
!parserfollowed by a triple-code block. - The bot compiles and executes instantly for quick experiments.
- When ready, save with
!cc create <name> <code>.
!parser ```function main() { ctx.reply("Hello, ElphoScript!");}```Tutorial steps (zero → advanced)
Step 1 Replies, variables, math
const math = use("math");
function main() { const roll = math.floor(math.random() * 100) + 1; ctx.reply("You rolled: " + roll);}How it works: use("math") loads 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
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
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
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
const embed = 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
const db = 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
const http = 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
const events = use("events");
function onMessage(ctx2) { if (ctx2.user.id === ctx.user.id) { ctx.reply("I hear you: " + ctx2.message.content); }}
function main() { 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 (dev) receives a message. We read ctx2 from the event but reply through the originalctx.
Step 9 Combine everything
const db = use("db");const http = use("http");const embed = use("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 loaded 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
const math = use("math");const embed = use("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());}const db = 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);}const http = use("http");const embed = use("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
const embed = 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
const db = use("db");const embed = use("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
"" + valueinstead 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
dband 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.