Send one notification first
Game.notify(message, groupInterval) sends a custom notification to the email channel configured for your Screeps profile. Start with one small call, then add rate limiting only when the condition can stay true across many ticks.
const result = Game.notify(
'A Screeps condition needs attention.',
30
);
console.log(result);
The official API documents a maximum message length of 1,000 characters, up to 20 notification calls in one game tick, and no availability in the Simulation Room. A groupInterval of 0 schedules immediately; a positive value groups notifications using that many minutes.
The public API page does not currently document a return-code table for Game.notify(). In the checked screeps/engine snapshot 80977824199a596d174d392fd0cf8c458c21fcbd, the method returns OK when the notification intent enters the per-tick queue and ERR_FULL when that 20-intent queue is full. Treat that return-code detail as an engine implementation boundary, not as a stronger API promise than the documentation makes.
Know the two different rate controls
| Control | What it does | What it does not do |
|---|---|---|
| 20 calls per tick | Official per-tick scheduling limit. | It does not stop one persistent condition from calling again on every later tick. |
groupInterval | Groups email notifications over a number of minutes. | It is not a replacement for deciding when your script should call Game.notify(). |
| Memory incident state | Project policy that suppresses repeated calls for the same unresolved condition. | It is not an official Screeps notification limit. |
For most alerts, the useful pattern is: call once when the condition becomes active, stay quiet while it remains active, and allow a new notification after the condition clears. Add a repeat interval only when repeated reminders are genuinely useful.
Preserve the call result
function sendNotification(message, groupInterval = 0) {
if (
typeof message !== 'string'
|| message.length === 0
|| message.length > 1000
) {
return { status: 'invalid-message' };
}
if (!Number.isFinite(groupInterval) || groupInterval < 0) {
return { status: 'invalid-group-interval' };
}
const result = Game.notify(message, groupInterval);
return {
status: result === OK
? 'notification-scheduled'
: 'notification-not-scheduled',
result
};
}
The length and non-negative interval checks are local guardrails around the documented parameters. The result === OK branch follows the checked engine behavior above. Preserve the raw result instead of turning every normal return into “delivered.”
Notify once per incident instead of once per tick
The helper below stores only the state needed for a recurring condition. active comes from your own alert rule. When the condition clears, the incident resets. A positive repeatAfterTicks is an optional project policy for reminders; it is measured in game ticks and is separate from the email-oriented groupInterval minutes.
function notifyIncident({
key,
active,
message,
groupInterval = 0,
repeatAfterTicks = null
}) {
if (typeof key !== 'string' || key.length === 0) {
return { status: 'invalid-incident-key' };
}
Memory.notificationIncidents ??= {};
const state = Memory.notificationIncidents[key] ??= {
active: false,
lastScheduledAt: null,
lastResult: null
};
if (!active) {
state.active = false;
return { status: 'incident-clear' };
}
const repeatDue = Number.isInteger(repeatAfterTicks)
&& repeatAfterTicks > 0
&& Number.isInteger(state.lastScheduledAt)
&& Game.time - state.lastScheduledAt >= repeatAfterTicks;
if (state.active && !repeatDue) {
return { status: 'already-reported' };
}
const submission = sendNotification(message, groupInterval);
state.lastResult = submission.result ?? null;
if (submission.status === 'notification-scheduled') {
state.active = true;
state.lastScheduledAt = Game.time;
}
return submission;
}
Do not copy a made-up room name or threshold into production. Pass a stable incident key, the condition you already calculated, and a message built from the real object or room you are monitoring. If the checked engine returns ERR_FULL, this helper leaves the incident eligible for a later retry instead of marking it as reported.
Scheduled is not externally delivered
Keep the evidence levels separate:
| Observation | What it supports | What it does not prove |
|---|---|---|
| Your condition became true | The local alert rule fired. | That Game.notify() was called. |
Checked engine returns OK | The notify intent entered the engine's per-tick queue. | That an external email arrived. |
Checked engine returns ERR_FULL | The checked per-tick notify queue was full. | That the message will be retried automatically. |
| Email or external inbox observation | External delivery evidence for that environment. | A general timing guarantee for every future notification. |
groupInterval can intentionally delay grouped mail. Game code has no documented inbox receipt, so do not name a Memory state delivered merely because the local call was accepted.
Optional hardening for many notification producers
If several modules can notify in the same tick, give them one shared local budget instead of letting every module assume it owns all 20 official slots. A production bot can choose a smaller local limit to reserve headroom.
function createNotificationBudget(maximumCalls = 20) {
const limit = Math.min(
20,
Math.max(0, Number.isInteger(maximumCalls)
? maximumCalls
: 20)
);
let scheduled = 0;
return {
send(message, groupInterval = 0) {
if (scheduled >= limit) {
return { status: 'local-budget-exhausted' };
}
const submission = sendNotification(message, groupInterval);
if (submission.status === 'notification-scheduled') {
scheduled += 1;
}
return submission;
},
getScheduledCount() {
return scheduled;
}
};
}
Create one budget object for the tick and route notification-producing modules through it. This is a project coordinator, not an extra Screeps API rule. Payload signing, approval workflows, revision ledgers, and external alert-service guarantees belong in a separate operations system when your threat model actually requires them.
Common mistakes
- Calling every tick while a condition stays true: use incident state so one unresolved condition does not keep scheduling calls.
- Treating
groupIntervalas a script-side cooldown: it controls email grouping in minutes; your own Memory state controls whether you call again. - Claiming delivery from a local result: accepted scheduling and external email observation are different evidence levels.
- Testing in the Simulation Room: the official API says
Game.notify()is not available there. - Scattered per-module call counting: use one shared coordinator when many producers can notify in the same tick.
- Overengineering a single alert: start with one call and one incident flag; add queues or richer identity only when the system actually needs them.
Evidence and production boundary
The current article was checked against the official Game.notify() API and the public screeps/engine snapshot 80977824199a596d174d392fd0cf8c458c21fcbd. The three JavaScript examples were syntax-checked offline. No real Screeps Console notification, 20-call saturation run, grouped-email timing trace, or inbox-delivery observation is claimed.