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

  1. Create a free cluster at MongoDB Atlas and a database user with a password.
  2. Allow connections from anywhere in Atlas network access (GuildScript's servers connect on your behalf).
  3. Copy the mongodb+srv://user:pass@cluster.mongodb.net/... connection string.
  4. Run /database in 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.

MethodReturnsNotes
db.findOne(coll, filter, options?)document or null
db.find(coll, filter, options?)array of documentsoptions 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 valuesCapped at 200 values.
db.exists(coll, filter)booleanCheapest way to check presence.
Guardrails

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

counter.js (event: messageCreate)
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`. updateOne with 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 guildId in your filters or use per-server collection names.
  • Trusting results blindly. A failed insert returns { error: "insert failed" }, not an exception. Check for .error on writes that matter.
  • Large scans. find returns 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.insertOne can 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).