Scripting Guide
Differences from discord.js
What carries over from discord.js, what is intentionally different, and a migration mapping for developers who know the library.
If you have written discord.js bots, GuildScript will feel familiar: builders have the same names and methods, events have the same names, and payload fields match discord.js properties closely. But agents are not discord.js programs. This page lists every difference that matters, and why it exists.
The big picture
| discord.js bot | GuildScript agent | |
|---|---|---|
| Process model | Long-running process you host | Short sandboxed run per event |
| Entry point | new Client() + client.login(token) | One exported async function |
| Events | client.on("event", fn), many per process | One event per agent, chosen at upload |
| State | In-memory caches and variables persist | Nothing persists between runs; use db |
| Objects | Class instances with managers and caches | Plain JSON snapshots with async action methods |
| Errors | Rejected promises throw | Actions return { error } instead of throwing |
| Network | Full Node.js: fetch, fs, npm | No network, no fs, no npm; db and llm helpers only |
There is no Client
The platform owns the Discord connection, the token, the intents, and the event loop. An agent never logs in and never registers listeners; it is the listener for the one event you selected. Anything in discord.js that hangs off client (client.user, client.guilds.cache, client.ws.ping, REST calls) has no equivalent, by design: an agent only sees the server it runs in.
// discord.js
const client = new Client({ intents: [GatewayIntentBits.GuildMessages, ...] });
client.on("messageCreate", async (message) => {
if (message.content === "!hi") await message.reply("Hello!");
});
client.login(process.env.TOKEN);
// GuildScript (upload on the messageCreate event)
export async function onMessage(message) {
if (message.author.bot) return;
if (message.content === "!hi") await message.reply("Hello!");
}Snapshots instead of class instances
Payload objects are JSON snapshots taken when the event fired, with async action methods attached. The practical consequences:
- No managers or caches. Where discord.js has
guild.members.cache(a Collection), GuildScript has explicit calls:guild.members.fetch(id),guild.members.list({ limit }), or plain arrays likemember.roleIds. - Collections become arrays. Anything iterable is a normal array:
message.attachments,mentions.users(an array of IDs),guild.channels.list(). - Data can go stale. A snapshot reflects the moment of the event. When freshness matters, call the object's
fetch()method to get an updated snapshot. - Actions return new snapshots.
channel.setName(...)resolves to the updated channel data rather than mutating the object you hold.
Errors are values, not exceptions
In discord.js a failed API call rejects and, unhandled, can crash your process. In GuildScript every Discord, database, and AI action catches its own failure and returns { error, code } (or a fallback like null). Your run continues; you decide what matters. Only bugs in your own code throw, and those are reported to the agent's error channel. There is rarely a reason to write try/catch in an agent.
What is intentionally restricted, and why
| Restriction | Reason |
|---|---|
No fetch or any network access | Agents from many servers share infrastructure; arbitrary outbound HTTP would enable abuse (SSRF, spam, data exfiltration). External calls exist only through the vetted llm helper and your own MongoDB via db. |
No fs, process, or environment access | There is no machine to access; the sandbox has no file system and secrets like tokens and keys must stay invisible to user code. |
No setTimeout / setInterval | Runs are short-lived by design; a sleeping run would hold a sandbox slot. Time-based logic uses stored timestamps compared on later events, or Discord-native timing like member.timeout(ms). |
No npm packages, no require, no eval | Only the audited "discord" module is available inside the sandbox; arbitrary code loading would defeat the sandbox. |
| Memory, time, and rate limits | Fair sharing between servers; see Lesson 12. |
| Reserved custom ID prefixes | Prevents agents from spoofing GuildScript's own management UI components. |
| Capped fetches (members 100, audit logs 50, find 200, ...) | Keeps any single run from monopolizing the Discord API or your database. |
| File/image URLs validated | Attachment and image URLs must resolve to public http(s) hosts so agents cannot probe internal networks. |
Migration mapping
| discord.js concept | GuildScript equivalent |
|---|---|
new Client() / client.login() | Not needed; export one async function |
GatewayIntentBits setup | Not needed; the platform manages intents |
client.on("messageCreate", fn) | Agent uploaded on the messageCreate event |
Several client.on(...) in one file | One agent per event (Lesson 13) |
message.reply() / channel.send() | Same names, same call shapes |
EmbedBuilder, ActionRowBuilder, ButtonBuilder, ... | Same builders, imported from "discord" instead of "discord.js" |
member.roles.cache.has(id) | member.roleIds.includes(id) |
member.roles.add(role) | member.roles.add(roleId) (IDs, not Role objects) |
guild.members.fetch(id) -> GuildMember | Same call, returns a member snapshot |
guild.channels.cache.find(...) | guild.channels.list() then Array.find |
interaction.options.getString("x") | Same; user/channel/role getters return IDs as strings |
interaction.deferReply({ ephemeral: true }) | Same |
awaitMessageComponent / collectors | Separate agent on interactionCreate routed by customId |
setTimeout(...) for delayed actions | Store a timestamp in db, act on a later event; or member.timeout(ms) for mutes |
fetch(...) to call an API | Not available; only llm.chat and db |
In-memory Map for state | db collections (Lesson 9) |
message.client.user.id | Not exposed; rely on message.author.bot filtering |
Permission checks via PermissionsBitField | member.permissions.includes("ManageMessages") (array of names) |
PartialMessage handling / .partial | Not needed; payloads arrive pre-fetched |
| Sharding, REST rate limit handling | Not your problem; the platform handles it |
Porting checklist
- Delete client setup, intents, login, and any
process.envusage. - Split each
client.onblock into its own file with one exported async function. - Change imports from
"discord.js"to"discord"and remove imports that no longer exist (Client, GatewayIntentBits, Partials). - Replace cache access (
*.cache) with payload fields,list()calls, orfetch()calls. - Replace in-memory state and timers with
dbdocuments and timestamps. - Replace thrown-error handling with result checks (
if (x?.error)). - Upload each file with
/agentson its matching event and set an error channel.
Assume the discord.js property name exists on the snapshot (it usually does: content, guildId, joinedTimestamp, displayName), and assume anything involving client, cache, collectors, or the outside world does not.