Build a configuration system for your scripts
Manage Northwind script settings without hardcoding — environment-aware config.
Published Oct 7, 2025
Every script accumulates settings — a batch size, a retry limit, a notification channel, an API endpoint. The lazy choice is to type these values straight into the code. It works until the day you need a different value in testing than in production, or you want to tune a job without editing and redeploying, or a teammate asks where a magic number came from.
A configuration system separates what the script does from how it is tuned. Done well, it lets one codebase run unchanged in development and production, lets non-developers adjust behaviour by editing a property, and gives tests a clean way to override settings without touching shared state. This guide builds a small, layered config helper that does all three.
Three sources, in priority order
A good config helper reads from several places and resolves conflicts with a clear precedence rule. The more specific the source, the higher it wins.
- Per-call overrides — a value passed directly at the call site. Highest priority, used almost entirely for tests.
- Script Properties — environment-specific values stored in the project. Different in the dev project and the prod project.
- Defaults in code — the fallback that ships with the source. Lowest priority, but always present so the script never crashes on a missing key.
This ordering means production behaviour lives in Script Properties, the code carries safe defaults, and a test can force any value it likes without disturbing either.
The helper
The whole system is two small functions. cfg() resolves a key against the
three layers; tryParse() lets stored values be numbers, booleans, or JSON,
not just strings.
// Code-level fallbacks. These ship with the source and are always available,
// so a missing Script Property can never crash the script.
const DEFAULTS = {
BATCH_SIZE: 100,
RETRY_LIMIT: 5,
SLACK_CHANNEL: '#alerts',
};
// Resolve one config key through all three layers, highest priority first.
function cfg(key, override) {
// Layer 1: an explicit override always wins. Used by tests.
if (override !== undefined) return override;
// Layer 2: an environment-specific value from Script Properties.
const p = PropertiesService.getScriptProperties().getProperty(key);
if (p !== null) return tryParse(p, p);
// Layer 3: the code default. Guaranteed to exist for every known key.
return DEFAULTS[key];
}
// Script Properties only store strings. Parse JSON so '100' becomes the
// number 100 and 'true' becomes the boolean true; fall back to the raw
// string for plain values like '#alerts'.
function tryParse(s, fallback) {
try { return JSON.parse(s); } catch { return fallback; }
}
Use it
Reading a setting is now a single call. Pass nothing for the normal case; pass a second argument only when a test needs to force a value.
// Normal use: resolves Script Property → default.
const batch = cfg('BATCH_SIZE');
// Test use: the third layer override wins over everything else.
const channel = cfg('SLACK_CHANNEL', '#dev');
Because tryParse() runs on stored values, a Script Property of 250 comes
back as a number and false comes back as a boolean — no manual Number() or
string comparison at the call site.
Set the properties
Script Properties live per project, so the dev project and the prod project hold different values for the same keys.
- Editor → Project Settings → Script Properties → add key/value pairs by hand for a quick change.
- Or set them in code once during setup, which is repeatable and reviewable.
// Run once per project to seed environment-specific values.
function seedConfig() {
PropertiesService.getScriptProperties().setProperties({
BATCH_SIZE: '250',
SLACK_CHANNEL: '#prod-alerts',
});
}
Why this pattern wins
| Approach | Change a value | Dev vs prod | Test override |
|---|---|---|---|
| Hardcoded constants | Edit + redeploy | Separate code | Not possible |
| Script Properties only | Edit a property | Per project | Awkward |
Layered cfg() helper | Edit a property | Per project | One argument |
- One codebase serves every environment — the difference lives in properties.
- Tuning a job is a property edit and a re-run, not a code change.
- Defaults in code mean a fresh project runs correctly before anything is set.
Common mistakes
- Storing secrets like API keys in
DEFAULTS. Code defaults end up in version control — keep secrets in Script Properties only. - Forgetting
tryParse()and comparingcfg('RETRY_LIMIT') > 3against a string. Without parsing, every stored value is text. - Calling
cfg()repeatedly inside a tight loop.getProperty()is a service call — read each key once before the loop and reuse the value. - Letting
DEFAULTSand the real keys drift apart. Every key the code reads should have a default, or a typo silently returnsundefined. - Using
cfg()overrides in production code as a shortcut. The override layer is for tests; production values belong in Script Properties.