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 botGuildScript agent
Process modelLong-running process you hostShort sandboxed run per event
Entry pointnew Client() + client.login(token)One exported async function
Eventsclient.on("event", fn), many per processOne event per agent, chosen at upload
StateIn-memory caches and variables persistNothing persists between runs; use db
ObjectsClass instances with managers and cachesPlain JSON snapshots with async action methods
ErrorsRejected promises throwActions return { error } instead of throwing
NetworkFull Node.js: fetch, fs, npmNo 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.

The same feature, both worlds
// 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 like member.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

RestrictionReason
No fetch or any network accessAgents 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 accessThere 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 / setIntervalRuns 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 evalOnly the audited "discord" module is available inside the sandbox; arbitrary code loading would defeat the sandbox.
Memory, time, and rate limitsFair sharing between servers; see Lesson 12.
Reserved custom ID prefixesPrevents 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 validatedAttachment and image URLs must resolve to public http(s) hosts so agents cannot probe internal networks.

Migration mapping

discord.js conceptGuildScript equivalent
new Client() / client.login()Not needed; export one async function
GatewayIntentBits setupNot needed; the platform manages intents
client.on("messageCreate", fn)Agent uploaded on the messageCreate event
Several client.on(...) in one fileOne 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) -> GuildMemberSame 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 / collectorsSeparate agent on interactionCreate routed by customId
setTimeout(...) for delayed actionsStore a timestamp in db, act on a later event; or member.timeout(ms) for mutes
fetch(...) to call an APINot available; only llm.chat and db
In-memory Map for statedb collections (Lesson 9)
message.client.user.idNot exposed; rely on message.author.bot filtering
Permission checks via PermissionsBitFieldmember.permissions.includes("ManageMessages") (array of names)
PartialMessage handling / .partialNot needed; payloads arrive pre-fetched
Sharding, REST rate limit handlingNot your problem; the platform handles it

Porting checklist

  1. Delete client setup, intents, login, and any process.env usage.
  2. Split each client.on block into its own file with one exported async function.
  3. Change imports from "discord.js" to "discord" and remove imports that no longer exist (Client, GatewayIntentBits, Partials).
  4. Replace cache access (*.cache) with payload fields, list() calls, or fetch() calls.
  5. Replace in-memory state and timers with db documents and timestamps.
  6. Replace thrown-error handling with result checks (if (x?.error)).
  7. Upload each file with /agents on its matching event and set an error channel.
When in doubt

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.