First mod
Folder, manifest and one skeleton script: an IIFE, onMapReady with an init guard, a stored unsubscribe and a notification. Plus best practices.
The task: a mod that greets the player when a city is on screen and pays a bonus every 100 days.
Create the Mod Folder#
Open the mods folder (Settings > Advanced > Mod Manager > Open Mods Folder; the paths are in Getting started) and create my-first-mod/ in it.
Add manifest.json#
{
"id": "com.yourname.my-first-mod",
"name": "My First Mod",
"version": "1.0.0",
"description": "Greets the player and pays a bonus every 100 days.",
"author": { "name": "Your Name" },
"main": "index.js"
}id, name and version are required. main defaults to index.js; author defaults to { "name": "Unknown" }. Getting started has the full field table.
Add index.js#
(function () {
const api = window.SubwayBuilderAPI;
let initialized = false;
let stopDayChange = null;
api.hooks.onMapReady(() => {
api.ui.showNotification('My First Mod is active', 'success');
// onMapReady fires again on every city load. Guard the one-time setup.
if (initialized) return;
initialized = true;
stopDayChange = api.hooks.onDayChange((day) => {
if (day % 100 === 0) {
api.actions.addMoney(1_000_000);
api.ui.showNotification(`Day ${day} bonus: $1,000,000`, 'success');
}
});
});
api.hooks.onGameEnd(() => {
if (stopDayChange) stopDayChange();
stopDayChange = null;
initialized = false;
});
})();What each part does#
The IIFE keeps api, initialized and stopDayChange out of the global scope. The loader already wraps the script in a strict-mode function; the IIFE keeps the code valid when pasted elsewhere.
`hooks.onMapReady` fires each time the map is created: a new game, a loaded save, and a game started after returning to the main menu. `reloadMods` replays it when the map is already up. Without the guard, each fire registers another onDayChange callback and the bonus is paid several times per day.
Every hooks.on* call returns an unsubscribe function. The skeleton stores the one from `hooks.onDayChange` and calls it in `hooks.onGameEnd`, which fires on a confirmed exit from the game screen (Exit to Menu or any other navigation away), on Exit to Desktop, before a save is loaded or imported from the pause menu, and when the tutorial is exited or completed.
`ui.showNotification` takes a message, a type ('info', 'success', 'error' or 'warning') and an optional title. A notification sent before the game screen first mounts is queued and shown when it does.
`actions.addMoney` adds to the balance. During an active daily challenge every actions call is blocked.
Enable the Mod#
- Settings > Advanced > Mod Manager. If the mod is missing, click the refresh icon (
Scan for new mods). - Toggle the mod on, click Reload Now.
- Start or load a city. The notification appears once the map is up.
- Edit
index.js, press Ctrl+Shift+R (Cmd+Shift+R on macOS). The script re-runs andonMapReadyis replayed.
A script that throws at load is logged as [ModLoader] Failed to execute mod <name>: and listed under Issues in the Mod Manager. Debugging has the common issues.
Mod Structure#
mods/
my-first-mod/
manifest.json
index.jsThe folder name is free; the loader identifies the mod by the manifest id. Other files in the folder are only read when the script loads them.
Conventions#
Wrap in IIFE#
Wrap the script in an immediately invoked function, as above, so its variables stay out of the global scope and two mods cannot collide on a name.
Use a Prefix for Logging#
console.log('[My First Mod] loaded');The API prefixes its own lines with [Modding API] and the loader with [ModLoader]; a mod prefix makes the mod's lines filterable in the console and in the log file, where they appear after [RENDERER].
Handle Errors Gracefully#
A throw in the script's top level stops the rest of it. A throw in a hook callback is recorded as a hook error, and the other callbacks still run. Wrap code that can fail:
(function () {
const api = window.SubwayBuilderAPI;
api.hooks.onCityLoad((cityCode) => {
try {
const stations = api.gameState.getStations();
console.log(`[My First Mod] ${cityCode}: ${stations.length} stations`);
} catch (error) {
console.error('[My First Mod] onCityLoad failed:', error);
}
});
})();Check API Availability#
The loader waits for window.SubwayBuilderAPI before running any mod, so a mod script can use it directly. Code that runs elsewhere (a script pasted early in the console, a shared helper) should check first:
if (!window.SubwayBuilderAPI) console.error('[My First Mod] SubwayBuilderAPI not found');Related Pages#
- Hooks: every game event a mod can react to.
- UI and Panels: buttons, panels and placements.
- Debugging: reload semantics, errors and log files.
- Mod repositories: ejfox/subwaybuilder-mod, ejfox/subway-builder-mods and Subway-Builder-Modded/template-mod.