Scripting Guide
Lesson 9: Storing Data with db
Connecting your MongoDB Atlas cluster and using the db helper: insert, find, update, delete, and upserts.
Why agents need a database
Agents keep nothing in memory between runs. Every run starts fresh: variables you set are gone the next time the event fires. Anything that must be remembered (scores, warnings, settings, cooldown timestamps) goes into your own MongoDB database through the db helper, the second argument of your agent function.
Connecting a database
- Create a free cluster at MongoDB Atlas and a database user with a password.
- Allow connections from anywhere in Atlas network access (GuildScript's servers connect on your behalf).
- Copy the
mongodb+srv://user:pass@cluster.mongodb.net/...connection string. - Run
/databasein your server, click Connect Database, and paste the string.
Only mongodb+srv:// Atlas URIs with a username and password are accepted. The string is stored encrypted, is never displayed again, and agent code never sees it; agents only get the db helper, which is already bound to your cluster. Without a connected database every db call returns an error value such as { error: "no database connected" }.
The db API
Every method takes the collection name first. Collections are created on first use; there is no schema to define.
| Method | Returns | Notes |
|---|---|---|
db.findOne(coll, filter, options?) | document or null | |
db.find(coll, filter, options?) | array of documents | options supports sort, limit (capped at 200), projection. |
db.insertOne(coll, doc) | { id } | |
db.insertMany(coll, docs) | { inserted, ids } | Max 100 documents per call. |
db.updateOne(coll, filter, update, options?) | { matched, modified } | options.upsert: true inserts when nothing matches. |
db.updateMany(coll, filter, update, options?) | { matched, modified } | |
db.deleteOne(coll, filter) / db.deleteMany(...) | { deleted } | |
db.countDocuments(coll, filter) | number or null | |
db.distinct(coll, field, filter?) | array of values | Capped at 200 values. |
db.exists(coll, filter) | boolean | Cheapest way to check presence. |
Queries time out after 3 seconds. The operators $where, $function, $accumulator, $expr, and mapReduce are stripped from filters and updates. On any failure, methods return a fallback (null, [], or { error }) instead of throwing, so check results when correctness matters.
A message counter
export async function onMessage(message, db) {
if (message.author.bot) return;
// Upsert: increment the author's count, creating the document on first use.
await db.updateOne(
"message_counts", // collection
{ userId: message.author.id }, // filter
{
$inc: { count: 1 }, // Mongo update operators work as usual
$set: { username: message.author.username, lastAt: Date.now() },
},
{ upsert: true },
);
if (message.content === "!mycount") {
const doc = await db.findOne("message_counts", { userId: message.author.id });
const count = doc ? doc.count : 0;
await message.reply("You have sent " + count + " counted messages.");
}
}Expected behavior: every human message silently increments a per-user document. !mycount replies with the author's running total, surviving restarts, re-uploads, and any amount of time, because the data lives in your cluster.
Filters and updates work like MongoDB
// Comparison and logical operators:
await db.find("warnings", { count: { $gte: 3 } });
await db.find("posts", { $or: [{ pinned: true }, { score: { $gt: 10 } }] });
// Sorting and limiting:
await db.find("scores", {}, { sort: { points: -1 }, limit: 10 });
// Common update operators:
await db.updateOne("profiles", { userId }, { $set: { bio: "hi" } });
await db.updateOne("profiles", { userId }, { $inc: { xp: 25 } });
await db.updateOne("profiles", { userId }, { $push: { badges: "helper" } });
await db.updateOne("profiles", { userId }, { $unset: { nickname: "" } });Common pitfalls
- Forgetting `upsert`.
updateOnewith no match does nothing unless{ upsert: true }; first-time users then never get a document. - Storing per-user data without the guild. If the bot runs in several of your servers against the same cluster, include
guildIdin your filters or use per-server collection names. - Trusting results blindly. A failed insert returns
{ error: "insert failed" }, not an exception. Check for.erroron writes that matter. - Large scans.
findreturns at most 200 documents and slow queries are cut at 3 s; design around indexes and tight filters, not full-collection scans. - Passing circular objects. Feeding the payload object itself into
db.insertOnecan crash the run; store plain fields you picked out yourself.
Exercise
Finish the !warn <id> command from Lesson 4: store warnings in a warnings collection keyed by { guildId, userId } with $inc: { count: 1 } and $push: { reasons: ... }, and make !warnings <id> reply with the count and the last three reasons (use sort and slice on the array you read back).