Elpho LogoElpho

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.
  • 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(...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.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
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

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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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());
}
javascript
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);
}
javascript
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

javascript
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

javascript
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 "" + 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.