Version-pin an Apps Script library so “latest” doesn’t break client scripts

TL;DR — Develop against HEAD, ship production clients on a numbered Apps Script library version, and roll forward deliberately — so a library tweak doesn’t silently break every spreadsheet that includes it.

Shared libraries are how bootstrappers stay DRY across client spreadsheets — until someone saves a breaking change and every file on “Head (development mode)” wakes up broken. The fix is cultural and mechanical: develop on HEAD, pin production to a numbered version, roll forward on purpose.

This is the library counterpart to versioned web app deployments: same instinct, different UI.

TL;DR

  • Library project: iterate freely on HEAD
  • Save a version (numbered) when the API is intentional
  • Client projects: identify the library by script id + version number, not Head
  • Breaking changes → new version + staged client updates
  • Keep a one-line changelog in the library README or a Versions note

Using the Apps Script editor a lot? NitroGAS drops free themes & snippets right into script.google.com — optional Co-Pilot when you want a boost.

Why Head feels fine until it isn’t

Head mode always loads the latest saved code. Great for you, the library author, testing in one harness sheet. Terrible as the default for twenty client files you don’t open daily:

  • A renamed function breaks menus overnight
  • A stricter validator rejects rows that used to pass
  • A new OAuth scope appears and users see unexpected consent

Version pins freeze the bytecode clients call until you choose otherwise.

Author workflow (library project)

  1. Build and test against HEAD in a dedicated harness spreadsheet.
  2. When behavior is good enough to share, File → Project properties / Deployments path you already use — or Save → Manage versions (classic editor: File → Manage versions) and note the version number + description.
  3. Announce the version in your internal changelog (“v42: add debounceOrSkip; tryLockOrSkip unchanged”).
// Library: LibCore.gs (development on HEAD) var LibCore = (function () { function debounceOrSkip(actionKey, ttlSeconds) { ttlSeconds = ttlSeconds == null ? 5 : ttlSeconds; var cache = CacheService.getScriptCache(); var token = 'debounce:' + actionKey; if (cache.get(token)) return false; cache.put(token, '1', Math.max(1, Math.min(ttlSeconds, 21600))); return true; } return { debounceOrSkip: debounceOrSkip // … stable exports only … }; })();

Export a small surface. Clients should call LibCore.debounceOrSkip, not rummage in private helpers you might rename tomorrow.

Client workflow (pin the version)

In each production spreadsheet / standalone script:

  1. Libraries → add library (script id of LibCore)
  2. Choose version N (not Head)
  3. Identifier: e.g. LibCore
// Client-bound script — pinned to library version 42 in the Libraries UI function onEdit(e) { if (!e || !e.range) return; if (!LibCore.debounceOrSkip('onEdit:Status', 3)) return; // … client-specific logic … }

There is no runtime setLibraryVersion() API worth depending on — the pin lives in the project’s library configuration. Treat changing it as a deploy step, same as bumping a dependency in package.json.

Deliberate roll-forward

When you need clients on a new version:

  1. Save library version N+1 with a clear note
  2. Update one canary client; smoke-test menus, triggers, and auth
  3. Roll the rest (checklist or script inventory)
  4. Leave stragglers on N until you confirm nothing still needs old signatures
/** * Optional runtime guard: library exposes its own version string. * Clients can log or assert during smoke tests. */ // In library: function getLibraryVersion() { return '42'; // bump when you save a version; documentation, not enforcement }

Semantic tips:

  • Additive changes (new functions) → safe to recommend early roll-forward
  • Behavioral changes (stricter validation) → call them out; may need client code edits first
  • Removals → keep a deprecated shim on N+1, remove on N+2

Head for authors, pins for production

Context Mode
Library harness sheet you own Head OK
Client files used by humans daily Numbered version
Workshop / throwaway demos Head OK if you say so out loud

If you must temporarily put a client on Head to debug, set a calendar reminder to pin it back. Head-as-debug is fine; Head-as-forgotten-production is how weekends burn.

Inventory beats memory

Keep a simple sheet or doc:

Client spreadsheet Library Pinned version Last rolled
Acme Pipeline LibCore 42 2026-10-15
Intake Form LibCore 41 2026-09-01

When you publish v43, you know who still needs attention.

Scopes and library versions

Adding a service call in the library can introduce new OAuth scopes. Clients pinned to an old version keep old scopes until they roll forward — then users may see a fresh consent screen. Call that out in the version note. Prefer additive scopes thoughtfully; surprise consent is how people disable your add-on mentally even when it’s “just a library.”

Naming and identifiers

Keep the library identifier stable across versions (LibCore). Don’t rename the identifier when you bump versions — clients break on LibCore.foo becoming LibCore2.foo for no benefit. New major libraries (genuinely incompatible API) deserve a new script project or a new identifier with a migration guide, not a silent Head flip.

Soft Co-Pilot note

NitroGAS helps pull shared helpers into a library-shaped project; Co-Pilot can draft the export wrapper — you still own version notes and which clients roll forward. Free extension; Co-Pilot optional.

Closing checklist

  • Library versions saved with human descriptions
  • Production clients on numbered versions (not Head)
  • Canary client before fleet roll-forward
  • Changelog / inventory of pinned versions
  • Breaking changes shimmed or communicated before removals

Happy Coding!