Subsystems & limits
The pieces of the script API that aren't a trigger, a ctx property, or a function on their own: outbound HTTP, the two configuration stores, caching, Twitch, how failures propagate, and the numeric budgets every tier enforces.
fetch()
The only network access scripts have; full signature on
Script functions.
HTTPS-only by default (HTTP is allowed only when the env record has
fetch_allow_http set), standard ports only,
GET/POST/PUT/PATCH/DELETE/HEAD only, private/internal addresses are blocked
(SSRF-guarded), and responses are auto-decompressed. A 5s default timeout, up to 15s
via optionsJson.timeoutMs, and a 1 MB response cap
(post-decompression). Request bodies are capped at 64 KB.
The allowed request headers are accept, content-type,
authorization, user-agent, client-id,
api-key, anthropic-version, and any x-* header;
anything else is dropped.
Two separate limits apply per guild: 30 requests/minute (a flat
sliding window, same on every tier) and an hourly call budget that scales by
tier (5/15/30/60 on Free/Plus/Pro/Ultra; see the tier table below),
exposed live as ctx.fetchCallsRemaining so a data-dependent loop can size
itself against what's left.
Environment variables
Guild-level configuration lives in a single key-value record with the key
env, whose value is a JSON object edited as one blob on the Database
page. Its keys are exposed to every script as ctx.env.* (e.g.
{"mod_log_channel_id":"123"} becomes
ctx.env.mod_log_channel_id), read-only during execution. Every value is a
string; non-string JSON values are stringified when read.
Secrets
Sensitive values like API keys live in a separate secrets record that
only guild administrators can view or edit; mods with script access
cannot. Scripts never receive the real values: each key is exposed as an opaque
placeholder token {{secret:NAME}} via ctx.secret.*, and the
real value is substituted server-side only inside fetch() (in the URL,
header values, or body). Use it directly, e.g.
authorization: "Bearer " + ctx.secret.MyKey. A key is present only once
an admin sets a non-empty value, so if (ctx.secret.MyKey) tests whether
it's configured. Because a script only ever holds the placeholder, logging it, sending
it in a message, or throwing it can never leak the real secret.
Key-value store
Each guild has its own key-value store
(getKV/setKV/deleteKV) with optional per-entry
expiry, browsable and editable as JSON on the Database page. env and
secrets are reserved system keys:
getKV/setKV/deleteKV all fail closed on them (a
script-visible error), so they can only be read through
ctx.env/ctx.secret and edited on the Database page.
Writes persist immediately. A setKV call lands in the
database the instant it returns, so a value it wrote survives even if the script
errors or times out later in the same run (see
Error handling).
Total storage per guild is capped by tier (5/15/50/100 MB; see the table below); a write that
would exceed the cap throws.
Message cache
An in-memory cache of every message seen in the last 30 minutes, scoped per guild and
per channel. It's used to recover best-effort deleted-message content on the
MessageDelete trigger, and to read a message's previous content
on MessageUpdate (Discord's edit event only carries the new version).
getMessage() and getMessages() do not read
it; they fetch live from Discord over REST (each counting as one rate-limited action),
so they return complete, authoritative history rather than only what the bot recently
saw. Being in-memory, a bot restart empties it, so a deleted/edited message right
after a restart has no recoverable content.
Twitch
Link a Twitch channel on the General page to enable the
TwitchStreamOnline, TwitchStreamOffline, and
TwitchChannelUpdate triggers. Just entering the broadcaster's channel
name is enough, no authorization needed. Those three dispatch immediately, fanned out
to every guild following that broadcaster with a matching enabled script.
TwitchChatBatch additionally needs a chat-reader authorization from the
streamer or one of their moderators. Chat is buffered per guild (up to 1,000 messages
per window; once full, the oldest plain message is dropped to make room; redemptions
and bit cheers are only evicted if literally everything buffered is one of those. The
drop count is reported as ctx.twitch.messagesDropped) and flushed as one
execution every ~10 minutes via ctx.twitch.messages, never per message,
so it can't burn through the hourly execution budget. Nightbot's messages are filtered
out before they reach a script. A channel-point text redemption arrives as a normal
chat message with rewardId set, and a bit cheer with bits
set, which is how redemption/cheer flows (e.g. a shoutout command) are scripted
without needing broadcaster-only scopes.
Status chips (Pending / Active / Error / Reauthorize Required / Not Connected) on the General page reflect the current connection state; a revoked authorization or a reader losing mod status flips the status and is cleaned up automatically within about a minute.
Error handling & guild isolation
Actions run live, in call order, the moment a script calls them. There's no queue and
no all-or-nothing rollback. A script that errors, times out, or exhausts a budget
partway keeps every action, KV write, and fetch it already made, and all of it counts
against the hourly budgets regardless of how the run ended. This is why work that must
happen later (lifting a temp ban, a reminder) is logged as intent to the KV
store and swept periodically by a Scheduled script, rather than the
script trying to "wait."
A failed Discord action normally throws and ends the script with an Error status. The
one exception is a target that simply isn't reachable: a user who doesn't accept DMs,
a member who already left, a message already deleted. Those outcomes are outside a
script's control, so sendDm, kick, timeout,
unban, and deleteMessage return false instead
of throwing, and the script keeps going (the REST call still counts against the hourly
action budget). ban is deliberately excluded from this list. Discord
allows banning a non-member fine, so a 404 there means a genuinely bad user ID.
Everything else still throws: a bad snowflake, a missing permission, an exhausted
budget, a target in another guild.
Every action a script takes is scoped to its own guild: channel-targeting functions validate the target actually belongs to the executing script's guild before acting, so a script cannot read or modify another server no matter what ID it's given.
Tiers & limits
Every tier includes the entire scripting engine: every trigger, function, and integration, from the start. What scales with tier is per-guild hourly budget and storage. Tiers are per guild, bought through Discord's store, and both upgrades and downgrades take effect immediately. Full pricing detail is on /pricing.
| Limit | Free | Plus | Pro | Ultra |
|---|---|---|---|---|
| Price / month | $0 | $2.99 | $5.99 | $14.99 |
| Script executions / hour | 2,000 | 5,000 | 15,000 | 30,000 |
| Discord actions / hour | 50 | 100 | 500 | 1,000 |
| fetch() calls / hour | 5 | 15 | 30 | 60 |
| Runtime budget / hour | 60s | 120s | 250s | 500s |
| Key-value storage | 5 MB | 15 MB | 50 MB | 100 MB |
| Max script length | 5,000 chars | 10,000 chars | 15,000 chars | 30,000 chars |
| Min scheduled interval | 30 min | 15 min | 10 min | 5 min |
Every cap above except the executions row is a rolling one-hour budget, checked
live per call and exposed to the running script
(ctx.fetchCallsRemaining, ctx.restActionsRemaining) so a
data-dependent loop can size itself against what's left. Usage against every cap
is shown on the guild's General page.
The executions row is a runaway/database-row backstop, not a budget to plan around. It's set well clear of normal traffic. Runtime and Discord actions are what actually bind first for a busy guild.
Two tier-independent, non-configurable backstops sit underneath all of this: Jint (the JS engine) enforces a 30-second wall-clock timeout per execution, a 50-level recursion limit, and a 100,000-statement cap. These protect the process itself regardless of tier, since one process can't offer unbounded wall-clock time no matter what a guild is paying for. Separately, Discord's own command-registration limits apply to every tier alike: 32-character command/option names, 100-character descriptions, and 25 options per command.