Skip to content
Skip to chapter

Chapter 31

Development and operations

Share a versioned Apps Script library

Build a reusable Google Apps Script library project, attach it by Script ID at a pinned version, and manage consumer upgrades and rollbacks.

By the end

Calculate a small request plan through a real library dependency and distinguish saved source, numbered versions, development mode, access, and consumer configuration.

Chapter navigation

Build a request-planning function once, then call a saved version from a separate Apps Script project. With the supplied fictional data, the first policy plans 10 points of work. A later policy plans 12. The consumer decides when to adopt that change and can select the earlier version again.

An Apps Script library is another script project whose public functions you include in your own project. Here the producer owns the calculation and releases. The consumer owns its input and chooses which release to call. Keep the projects separate: copying the calculation into the consumer would hide the version and access boundaries you need to learn.

You need a Google account that can create and run private Apps Script projects. Use two new standalone projects, not a production automation. There is no spreadsheet, OAuth client, API key, npm installation, or paid service to configure. The data is in the source, and each run writes only an execution-log result.

Decide what belongs in the library

The fictional request list has three records:

IDStatusWork points
REQ-001Ready3
REQ-002Hold2
REQ-003Ready5

The library plans only Ready work. In its first policy, each Ready request also needs one coordination point. Two Ready requests contribute eight work points and two coordination points, for a plan of ten. A Hold request still has to be a valid record, but contributes no work or coordination points.

planRequests(requests) accepts an array of zero to 100 objects. Each object needs a unique id in the form REQ-001, a status of exactly Ready or Hold, and numeric integer points from zero through 100. Extra fields are ignored. An empty list is valid; missing input, repeated IDs, numeric text, unknown statuses, and malformed records are not. The function validates every record and either returns a complete result or throws an error. It does not quietly drop invalid requests to produce a plausible smaller total.

The result is a new object with requestCount, readyCount, workPoints, coordinationPoints, and plannedPoints. The input array and its records are not changed. This is a pure calculation: for the same accepted input and library policy, it returns the same numbers without reading a sheet, using the clock, or storing a counter. That makes it useful to several consumers without making their file selection, identities, or permissions part of the calculation.

getRelease() returns a source label such as request-plan-1. The label helps a consumer reject an unexpected policy. It is not an Apps Script version number, is not assigned by Google, and does not prove which bytes were deployed. The producer maintainer must keep the label and policy change together.

Create the producer project

Standalone scripts are independent of a Sheet or Doc and appear as files in Drive. Use a standalone producer so you can review and share the library without sharing a spreadsheet container.

  1. Open the Apps Script dashboard (script.google.com/home) with your practice account and click New project.

  2. Name the project RequestPlan. Keep it private and outside a folder with unintended inherited access.

  3. Replace the starter Code.gs with the producer source below. This file belongs only in the producer.

  4. In Project Settings, select Show "appsscript.json" manifest file in editor. Return to Editor, open that file, and replace it with the producer manifest below. Save both files.

  5. Select previewLibrary_ in the function dropdown and click Run. Open Execution log. The local preview supplies the three records and calls the calculation with arguments. Do not run planRequests directly from the dropdown: an editor run supplies no request array.

Code.gs
// Change both constants together for the chapter's second release.
const REQUEST_PLAN_RELEASE_ = 'request-plan-1';
const COORDINATION_POINTS_PER_READY_ = 1;
const MAX_REQUESTS_ = 100;

/**
 * Returns this source's policy label, not its Apps Script version number.
 * @return {string} The label recorded by the library author for this release.
 */
function getRelease() {
  return REQUEST_PLAN_RELEASE_;
}

/**
 * Plans work for Ready requests without changing the input or using services.
 * Accepts zero to 100 records. IDs must be unique REQ- followed by three digits;
 * status must be Ready or Hold; points must be an integer from 0 through 100.
 * Additional fields are ignored. Invalid records stop the whole calculation.
 * @param {Array<{id: string, status: string, points: number}>} requests Request records.
 * @return {{requestCount: number, readyCount: number, workPoints: number, coordinationPoints: number, plannedPoints: number}} Counts and points for the plan.
 * @throws {Error} If the list or a required field is invalid.
 */
function planRequests(requests) {
  if (!Array.isArray(requests) || requests.length > MAX_REQUESTS_) {
    throw new Error('RequestPlan input: supply an array of at most 100 requests.');
  }

  const seenIds = new Set();
  let readyCount = 0;
  let workPoints = 0;
  for (let index = 0; index < requests.length; index += 1) {
    const request = requests[index];
    validateRequest_(request, index + 1);
    if (seenIds.has(request.id)) {
      throw new Error('RequestPlan input: duplicate ID at request ' + (index + 1) + '.');
    }
    seenIds.add(request.id);
    if (request.status === 'Ready') {
      readyCount += 1;
      workPoints += request.points;
    }
  }

  const coordinationPoints = readyCount * COORDINATION_POINTS_PER_READY_;
  return {
    requestCount: requests.length,
    readyCount,
    workPoints,
    coordinationPoints,
    plannedPoints: workPoints + coordinationPoints,
  };
}

/**
 * Validates one record. The trailing underscore keeps this out of the library API.
 * @param {{id: string, status: string, points: number}} request A request record.
 * @param {number} position The one-based position used in an error message.
 */
function validateRequest_(request, position) {
  const prefix = 'RequestPlan input: request ' + position + ' ';
  if (!request || typeof request !== 'object' || Array.isArray(request)) {
    throw new Error(prefix + 'must be an object.');
  }
  if (typeof request.id !== 'string' || !/^REQ-[0-9]{3}$/.test(request.id)) {
    throw new Error(prefix + 'needs an ID such as REQ-001.');
  }
  if (request.status !== 'Ready' && request.status !== 'Hold') {
    throw new Error(prefix + 'status must be Ready or Hold.');
  }
  if (!Number.isInteger(request.points) || request.points < 0 || request.points > 100) {
    throw new Error(prefix + 'points must be an integer from 0 through 100.');
  }
}

/**
 * Runs the supplied fictional fixture inside the producer project only.
 * This editor entrypoint is not exported to library consumers.
 */
function previewLibrary_() {
  const requests = [
    { id: 'REQ-001', status: 'Ready', points: 3 },
    { id: 'REQ-002', status: 'Hold', points: 2 },
    { id: 'REQ-003', status: 'Ready', points: 5 },
  ];
  const plan = planRequests(requests);
  Logger.log(JSON.stringify({ release: getRelease(), plan }));
}

Download Code.gs
appsscript.json
{
  "timeZone": "Etc/UTC",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": []
}

Download appsscript.json

The expected log message is the following JSON. The editor adds its own time and severity decorations around messages.

Source
{"release":"request-plan-1","plan":{"requestCount":3,"readyCount":2,"workPoints":8,"coordinationPoints":2,"plannedPoints":10}}

Both manifests use V8 and an empty oauthScopes array. The calculation uses JavaScript only; the preview adds Logger.log. This source does not need permission to read Drive, Sheets, or Gmail or to make external HTTP requests. View scopes under Overview > Project OAuth Scopes. If a copied project or dependency asks for unexpected access, inspect its source and manifest rather than approving a broader permission to get past the prompt. Signing into Apps Script and permission to edit a project are not OAuth data scopes.

Running the preview records an execution and a log. It does not create a library version or prove that a separate consumer can resolve the library.

Make only the intended functions public

The producer exports getRelease and planRequests as top-level function declarations. Apps Script's library visibility rules expose enumerable global properties, including ordinary function declarations and globals declared with var. End a function name with an underscore to exclude it from the library API. That is why validateRequest_ and previewLibrary_ are callable inside the producer but not through the consumer's library identifier.

A top-level const or let is not the same enumerable global property as a var declaration. Do not refactor the public functions into top-level const arrow functions and assume the library API is unchanged because the editor can run them. The V8 editor's function detection and library export visibility are different contracts. The private constants in this producer are implementation details; consumers obtain the release through a function instead of reading a global variable.

The JSDoc comments describe parameters, return fields, and failures. Apps Script uses these comments for library completion and reference documentation. They do not validate data at runtime; the function body does that. In the consumer editor, the library's More menu > Open in a new tab opens its reference documentation.

An underscore is an API boundary, not a secret store. Library users need access to the producer source, so they can read the private helper's implementation. Do not put credentials or confidential data in a library and expect the naming convention to hide them.

Save a numbered library release

Head means the producer's latest saved editor code. A version is an immutable snapshot, identified by a number assigned within that script project. A deployment makes a version available for use. These are distinct from the request-plan-1 label in your source. Google's versions guide explains the snapshot boundary; the library quickstart gives the current Library deployment steps.

  1. Confirm that the producer's two policy constants still contain request-plan-1 and 1, and that its preview has the expected ten-point result.

  2. Choose Deploy > New deployment. Next to Select type, open Enable deployment types and select Library.

  3. Enter a description identifying the policy, such as request-plan-1: one coordination point per Ready request. Users with access can see this description. Click Deploy.

  4. Record the actual numbered version in Deploy > Manage deployments or Project History. The first release in a fresh project is normally version 1; use the number you actually see. Call this recorded number N1 in your notes. N1 is notation in this lesson, not a value to paste into the manifest.

  5. In the producer's Project Settings > IDs, copy the Script ID privately for the next section.

The consumer needs the producer's Script ID, not the deployment ID or a deployment URL. The project name only suggests a default identifier. A Google Cloud project number, OAuth client ID, or application release label cannot identify an Apps Script library.

Keep the association between the producer Script ID, N1, its policy label, and the inspected source in your release notes. A string returned by getRelease() can catch a mistake, but any different implementation could return that same string. To establish what a numeric release contains, inspect the corresponding source under Project History and observe the real consumer using that selection. A local source archive alone cannot establish authenticated resolution or the bytes behind a deployed number.

Grant access without publishing the source to everyone

If one account owns both practice projects, it already has access. Leave General access as Restricted; no share is needed to complete the baseline example.

If a second account will run the consumer, give that account Viewer access to the standalone producer. In Drive, select the RequestPlan script file, choose Share, enter only the intended account, and select Viewer. Leave general access restricted. These are the current specific-person sharing steps.

Every account that runs this practice consumer needs at least view/read access to the producer. Sharing only the consumer does not grant that access. Google's collaboration guide states that a collaborator without read-level library access cannot use it and gets a script error. A Script ID is an identifier, not an access credential.

Give producer Editor access only to maintainers. Editors can change source and create new versions; an ordinary consumer does not need those powers. Viewers are enough for numbered releases, while the library-specific head testing instructions require editor access. That distinction matters when you try development mode later.

Access and availability remain dependencies even with a numeric pin. Keep a maintainer and a retention plan for the producer and old versions. Removing access or deleting the producer can break consumers; a saved consumer manifest is not a self-contained copy of the library.

Install the library in a separate consumer

Create the second standalone project before adding any dependency:

  1. Return to the Apps Script dashboard, click New project, and name this project RequestPlan practice.

  2. Paste only the consumer Code.gs below. Do not paste the producer's planRequests, getRelease, or validation helper into it.

  3. Reveal appsscript.json in Project Settings and install the consumer base manifest below. Save.

Code.gs
// This policy label is checked after the real library loads.
// It does not configure or prove the installed Script ID, version, or source.
const EXPECTED_LIBRARY_RELEASE_ = 'request-plan-1';

/** Calls the installed RequestPlan library with this consumer's fictional data. */
function runLibraryConsumer() {
  if (typeof RequestPlan === 'undefined') {
    throw new Error('Install the producer library as RequestPlan before running.');
  }
  if (typeof RequestPlan.getRelease !== 'function' || typeof RequestPlan.planRequests !== 'function') {
    throw new Error('The installed RequestPlan library does not expose the required public functions.');
  }
  const release = RequestPlan.getRelease();
  if (release !== EXPECTED_LIBRARY_RELEASE_) {
    throw new Error('Library policy label mismatch. Review the installed version and expected release before running.');
  }

  const requests = sampleRequests_();
  const plan = RequestPlan.planRequests(requests);
  validatePlan_(plan);
  Logger.log(JSON.stringify({ release, plan }));
}

/** Returns fresh fictional input on every run; no spreadsheet is read. */
function sampleRequests_() {
  return [
    { id: 'REQ-001', status: 'Ready', points: 3 },
    { id: 'REQ-002', status: 'Hold', points: 2 },
    { id: 'REQ-003', status: 'Ready', points: 5 },
  ];
}

/**
 * Checks the consumed result contract without reimplementing request planning.
 * @param {{requestCount: number, readyCount: number, workPoints: number, coordinationPoints: number, plannedPoints: number}} plan The returned plan.
 */
function validatePlan_(plan) {
  if (!plan || typeof plan !== 'object' || Array.isArray(plan)) {
    throw new Error('The library did not return a plan object.');
  }
  const counts = [plan.requestCount, plan.readyCount, plan.workPoints, plan.coordinationPoints, plan.plannedPoints];
  if (!counts.every((value) => Number.isInteger(value) && value >= 0) ||
      plan.requestCount > 100 || plan.readyCount > plan.requestCount ||
      plan.plannedPoints !== plan.workPoints + plan.coordinationPoints) {
    throw new Error('The library returned an incompatible plan. Review the selected version before using the result.');
  }
}

Download Code.gs
appsscript.json
{
  "timeZone": "Etc/UTC",
  "dependencies": {
    "libraries": []
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": []
}

Download appsscript.json

The supplied manifest has an empty libraries array because your producer's Script ID and numeric release do not exist until you create them. Before installation, runLibraryConsumer stops with Install the producer library as RequestPlan before running. That guard does not supply a substitute implementation. Apps Script must create the real RequestPlan binding through its library configuration.

Now use the documented library installation flow:

  1. In the consumer editor, click + next to Libraries.

  2. Paste the producer's exact Script ID in Script ID and click Look up. Check the returned project rather than selecting an unrelated library with a similar name.

  3. In Version, choose the actual number recorded as N1. Do not select HEAD (Development Mode).

  4. Set Identifier to exactly RequestPlan, including its capitalization. Click Add.

  5. Open the consumer's manifest and inspect the single entry now present in dependencies.libraries. Check each field against the following table. If the editor omits developmentMode, add it with value false in that library entry and save.

FieldRequired value for this runWhat it controls
libraryIdThe actual producer Script IDWhich project supplies code
versionThe actual N1 number as a JSON string, for example "1" only if that is your recorded numberWhich snapshot is selected
userSymbol"RequestPlan"The name used by this consumer's JavaScript
developmentModefalseHonor the version instead of running current producer code

These are the actual library manifest fields, not advanced-service configuration. That reference also allows the symbolic value stable; this exercise uses an explicit number so the intended snapshot is recorded. Keep the rest of the manifest, including the empty scopes array. Once you configure the dependency, replacing the manifest with the original download would remove the installation.

Select runLibraryConsumer, click Run, and inspect Execution log. With the original fixture and first release, expect the same JSON result as the producer preview: three requests, two Ready, eight work points, two coordination points, and ten planned points. Run it again without changes. It should produce the same result, not a total that grows with each run.

RequestPlan.planRequests(requests) is a call through the installed library identifier. The name RequestPlan comes from the Identifier you set during installation, not from an ES module import, an object defined in the consumer, or the producer's project title. The consumer performs one release-label call and one planning call. It checks the returned fields before logging them, but does not repeat the library's aggregation or fall back to a copied calculation.

An installed library can fail to load before your entrypoint starts, for example because its ID, version, or access is wrong. In that case the local typeof RequestPlan guard cannot replace the platform error. Fix the dependency or permissions rather than adding a fake local RequestPlan object to make the name exist.

Change the input without releasing the library

The consumer owns the data. Find the REQ-002 record in its sampleRequests_ function and change status: 'Hold' to status: 'Ready'. Before running, predict which fields change. The number of records stays three; the second request now contributes two work points and one coordination point.

Save and run runLibraryConsumer while it still selects N1. Expect:

Source
{"release":"request-plan-1","plan":{"requestCount":3,"readyCount":3,"workPoints":10,"coordinationPoints":3,"plannedPoints":13}}

Restore Hold, save, and run again. The plan returns to ten. This is a consumer source edit, not a library change, so there is no new producer version to create. Editing the producer preview's fixture would not change this consumer's input either; the arrays belong to separate callers.

Next, with the original statuses restored, change only the second record's numeric points: 2 into points: '2'. This remains valid JavaScript but violates the library's input contract. Run once. Expect RequestPlan input: request 2 points must be an integer from 0 through 100. and no JSON plan. The library validates Hold records too. Restore numeric 2, save, and rerun to recover.

You can also replace the returned fixture array with an empty array. The result is a valid zero-request plan with all five numeric fields zero. Restore the supplied records before the version exercise so input changes do not obscure the release behavior.

Save a change, then prove the consumer is still pinned

The next policy allows two coordination points for every Ready request. It keeps the public function names, input rules, and output fields, but changes the meaning of the resulting budget. A compatible data shape does not make a policy change harmless to its callers.

In the producer's current Code.gs, change COORDINATION_POINTS_PER_READY_ from 1 to 2 and REQUEST_PLAN_RELEASE_ from request-plan-1 to request-plan-2. Save. Run previewLibrary_ in that producer. With its unchanged three records, expect four coordination points and twelve planned points under the second label.

Do not create a new version yet. Return to the consumer. Keep its dependency at N1, developmentMode false, its expected label at request-plan-1, and its original fixture. Run again. Expect ten points and the first label. The head deployment follows saved edits, but the consumer's selected numbered snapshot has not changed.

If the consumer shows the new policy or a label mismatch already, stop and inspect its dependency. Development mode may be on, the selected number may not be the one you recorded, or the first snapshot may not contain the source you thought it did. Changing the expected label only to silence the error would hide the evidence you are trying to observe.

Release, upgrade, and roll back versions

To make the second policy available as a numbered release, use the producer:

  1. Confirm its saved source and preview are the intended second policy.

  2. Choose Deploy > Manage deployments, select the Library deployment, and click Edit.

  3. Under Version, select New version. Enter a description identifying request-plan-2 and its two-point allowance, then click Deploy.

  4. Record the actual new numeric version as N2. View the old and new snapshots under Project History; Highlight changes compares a chosen snapshot with current head. Do not assume N2 is 2 if other versions were created in between.

This follows Google's create-version procedure. The first snapshot stays immutable. Updating a library deployment to a new version is not the same operation as editing every consumer's dependencies.libraries selection.

The consumer maintainer owns the upgrade. In this practice project there are no triggers or external callers; stop manual runs while changing the dependency and expected label together. For a real scheduled or deployed consumer, its owner must plan how to pause or release that configuration change without exposing a half-updated program.

  1. In the consumer, under Libraries, click RequestPlan. Change Version to the actual N2 number, leave development mode off, and click Save.

  2. Set EXPECTED_LIBRARY_RELEASE_ in the consumer source to request-plan-2 and save. Check the manifest's ID, version, identifier, and developmentMode again.

  3. Run the original fixture. Expect:

Source
{"release":"request-plan-2","plan":{"requestCount":3,"readyCount":2,"workPoints":8,"coordinationPoints":4,"plannedPoints":12}}

If you change only the selected version while leaving the old expected label, the consumer stops with a policy-label mismatch. The check is useful because the function signatures alone would not expose the budget change. A matching label still does not establish authenticity or prove the whole result is correct; retain the inspected source and compare the actual numbers.

Now demonstrate a rollback. In Libraries > RequestPlan, select the saved N1 number and Save. Keep development mode off, restore EXPECTED_LIBRARY_RELEASE_ to request-plan-1, and run the unchanged fixture. Expect ten points again. You did not need to rewrite the producer's newer head or create another version to choose an existing snapshot.

Rollback affects this consumer's future calls. It does not update a different consumer, erase a log, or undo effects a less limited library previously performed. Keep a record of consumers, owners, selected versions, and release behavior. Do not delete older versions or remove producer access while another project may still depend on them.

Use development mode only for a controlled experiment

With at least one saved version, the producer owner or an editor can select HEAD (Development Mode) in the consumer's library settings. Google's library testing contract gives this access condition. Do not grant someone Editor so an ordinary pinned consumer can run.

Development mode sets developmentMode to true. The manifest contract says that this ignores version and uses the current library project code. A numeric-looking version field left in the manifest is therefore not a pin while that flag is true.

For the optional experiment, keep the producer head on request-plan-2. In the practice consumer, select HEAD (Development Mode) and set its expected label to request-plan-2. Run the original fixture and expect twelve. This observes saved head code without requiring another numbered release for each edit. If you temporarily change the producer's allowance from 2 back to 1, save, and rerun that HEAD consumer, the planned points should become ten even though its release string still says request-plan-2. Restore the allowance to 2 immediately afterward. This shows that head is mutable and that a label does not prove behavior. Do not create a numbered release of that temporary mismatched source.

Finish by selecting a numbered version, setting developmentMode false, and restoring the matching expected label. Check the original fixture result again. Leaving a shared consumer on development mode lets a later producer save change its behavior without the consumer maintainer choosing an upgrade.

Know what the library does not isolate

Calling a library is not a deployment that runs as its owner. Here the consumer's manual execution invokes the library with explicit plain input; it borrows no producer-owner access to a spreadsheet. A library that starts using services must be reviewed for its effects, scopes, and resource ownership before consumers adopt it.

Libraries are also not independent containers for every resource. The resource-scoping table distinguishes resources tied to the library from resources shared with the invoking script. For example, the library's Script Properties are its own store, used across its consumers; User Properties and logging follow the shared-resource rules. Do not move a helper that reads or writes properties into a library and assume its state boundary stays unchanged. This example avoids that problem by using no properties, cache, locks, active container, or triggers at all.

The library code is a trusted dependency. Its access to input is real, and a release can introduce side effects even when its function names do not change. An underscore, the consumer's result-shape check, and an empty scopes list are not a general sandbox for arbitrary code. Review the actual producer and release before installation, keep the consumer's privileges narrow, and treat new service requirements as a new decision.

Libraries also add invocation overhead. Google's library guide cautions against excessive library use in latency-sensitive and UI-heavy code. Pass a bounded array for one calculation instead of repeatedly crossing the library boundary for every cell. For a single tiny helper in one project, a maintained library release process may cost more than it solves; this exercise is useful when several consumers need a shared policy.

Keep Apps Script libraries separate from npm dependencies

Apps Script's V8 runtime does not support ES module import or export, and it is not a Node.js package manager. An npm package name or a package.json entry does not create RequestPlan in the consumer.

QuestionApps Script library in this lessonCompatible npm code bundled locally
Where does the dependency come from?A separate script project selected by Script ID and numbered versionPackage source resolved by local build tooling
What does the consumer ship?A library dependency in appsscript.json plus calls through its identifierEmitted JavaScript containing the selected dependency code
Who can resolve it?The using account needs library project accessThe deployed bundle does not resolve an npm package at execution time
What changes its code?Explicit version selection, or producer saves if development mode is onA reviewed dependency/build change followed by uploading the emitted code
What still needs review?Export surface, project access, scopes, resource behavior, and release ownershipDependency source, runtime compatibility, bundled output, scopes, and upload target

Type declarations help local tools understand names and data shapes. They do not install a library, grant access, or implement its functions. Neither an any declaration nor a local no-op object can stand in for the library configuration. A bundler must also produce code compatible with Apps Script rather than leaving Node-only APIs or unresolved imports in the upload. The local-development workflow is a separate way to manage source; it does not change these runtime contracts.

Diagnose the boundary that failed

ObservationCheck before rerunning
Look up cannot find the producerVerify the producer Script ID from Project Settings, the signed-in account, and at least Viewer access. Do not paste a deployment ID or broaden general access.
No suitable numbered version is availableConfirm the producer's Library deployment created a version. Saving source alone does not create one.
The platform fails before runLibraryConsumer logs anythingInspect the installed library's ID, availability, version, and this execution account's access. A consumer collaborator still needs producer read access.
Install the producer library as RequestPlan before running.Install the real dependency or restore its identifier. Do not copy the producer implementation into the consumer.
Required public functions are missingInspect the selected snapshot and public function declarations. Check spelling, underscores, and whether a refactor changed export visibility.
Policy-label mismatchCompare the selected numeric version, development flag, expected label, and producer snapshot. Do not change the expectation without reviewing the release.
The log changes after only a producer saveCheck for development mode or a mistaken version selection. A label alone is not a pin.
RequestPlan input errorFix the indicated consumer record. Use numeric integers, recognized statuses, and unique correctly shaped IDs; restore the original fixture to separate data errors from release errors.
An incompatible-plan errorStop using the result. Inspect the selected library's return contract and source rather than filling missing fields with defaults.
Breakpoints in the consumer do not enter the libraryThis is a documented debugger limitation, even in development mode. Debug previewLibrary_ inside the producer with the same input instead.

For the last case, Google's library debugging instructions say that stepping into library code and hitting its breakpoints from the consumer are not supported. A successful producer preview tests current producer source, not necessarily the numeric version installed by the consumer; compare both when diagnosing a release problem.

An identifier rename is another consumer-owned change. As a short experiment, rename the installed identifier to WorkPlan, save, and run unchanged code. It should fail to find RequestPlan. Restore the identifier and rerun. If you intend a permanent rename, update all identifier references, including the guard, method checks, and calls. Renaming the producer project itself does not update an existing consumer's chosen identifier.

There is no automatic retry here. A calculation error is not transient; a version or access problem needs correction. After correcting it, running the same pure fixture again does not duplicate a business action or accumulate state. The hosting platform can still fail an execution or log operation; do not equate the absence of output with proof that no calculation happened.

Leave a deliberate dependency behind

Restore the original consumer fixture, including the numeric 2 and Hold status. Restore any identifier experiment to RequestPlan. Choose the numbered version with development mode off, pair it with the matching expected label, and check the result once more. Keep both producer versions if you want to retain the upgrade and rollback path.

If you no longer need this practice consumer, remove its dependency using Libraries > the library's More menu > Remove > Remove library, following the library removal instructions. Removing the dependency does not delete the producer or remove another account's access. A later run without reinstallation reaches the missing-library guard.

Do not delete the producer, erase versions, or revoke sharing while consumers may rely on them. The example created no tokens, schedules, spreadsheet output, or persistent counters to clean up.

Next, manage a dedicated OAuth 2.0 grant for a separate account.

Project files

Complete source files for this chapter’s examples.

Produce a versioned request-planning library
Code.gs
// Change both constants together for the chapter's second release.
const REQUEST_PLAN_RELEASE_ = 'request-plan-1';
const COORDINATION_POINTS_PER_READY_ = 1;
const MAX_REQUESTS_ = 100;

/**
 * Returns this source's policy label, not its Apps Script version number.
 * @return {string} The label recorded by the library author for this release.
 */
function getRelease() {
  return REQUEST_PLAN_RELEASE_;
}

/**
 * Plans work for Ready requests without changing the input or using services.
 * Accepts zero to 100 records. IDs must be unique REQ- followed by three digits;
 * status must be Ready or Hold; points must be an integer from 0 through 100.
 * Additional fields are ignored. Invalid records stop the whole calculation.
 * @param {Array<{id: string, status: string, points: number}>} requests Request records.
 * @return {{requestCount: number, readyCount: number, workPoints: number, coordinationPoints: number, plannedPoints: number}} Counts and points for the plan.
 * @throws {Error} If the list or a required field is invalid.
 */
function planRequests(requests) {
  if (!Array.isArray(requests) || requests.length > MAX_REQUESTS_) {
    throw new Error('RequestPlan input: supply an array of at most 100 requests.');
  }

  const seenIds = new Set();
  let readyCount = 0;
  let workPoints = 0;
  for (let index = 0; index < requests.length; index += 1) {
    const request = requests[index];
    validateRequest_(request, index + 1);
    if (seenIds.has(request.id)) {
      throw new Error('RequestPlan input: duplicate ID at request ' + (index + 1) + '.');
    }
    seenIds.add(request.id);
    if (request.status === 'Ready') {
      readyCount += 1;
      workPoints += request.points;
    }
  }

  const coordinationPoints = readyCount * COORDINATION_POINTS_PER_READY_;
  return {
    requestCount: requests.length,
    readyCount,
    workPoints,
    coordinationPoints,
    plannedPoints: workPoints + coordinationPoints,
  };
}

/**
 * Validates one record. The trailing underscore keeps this out of the library API.
 * @param {{id: string, status: string, points: number}} request A request record.
 * @param {number} position The one-based position used in an error message.
 */
function validateRequest_(request, position) {
  const prefix = 'RequestPlan input: request ' + position + ' ';
  if (!request || typeof request !== 'object' || Array.isArray(request)) {
    throw new Error(prefix + 'must be an object.');
  }
  if (typeof request.id !== 'string' || !/^REQ-[0-9]{3}$/.test(request.id)) {
    throw new Error(prefix + 'needs an ID such as REQ-001.');
  }
  if (request.status !== 'Ready' && request.status !== 'Hold') {
    throw new Error(prefix + 'status must be Ready or Hold.');
  }
  if (!Number.isInteger(request.points) || request.points < 0 || request.points > 100) {
    throw new Error(prefix + 'points must be an integer from 0 through 100.');
  }
}

/**
 * Runs the supplied fictional fixture inside the producer project only.
 * This editor entrypoint is not exported to library consumers.
 */
function previewLibrary_() {
  const requests = [
    { id: 'REQ-001', status: 'Ready', points: 3 },
    { id: 'REQ-002', status: 'Hold', points: 2 },
    { id: 'REQ-003', status: 'Ready', points: 5 },
  ];
  const plan = planRequests(requests);
  Logger.log(JSON.stringify({ release: getRelease(), plan }));
}

appsscript.json

Download file
appsscript.json
{
  "timeZone": "Etc/UTC",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": []
}

Consume an explicitly selected request-planning library version
Code.gs
// This policy label is checked after the real library loads.
// It does not configure or prove the installed Script ID, version, or source.
const EXPECTED_LIBRARY_RELEASE_ = 'request-plan-1';

/** Calls the installed RequestPlan library with this consumer's fictional data. */
function runLibraryConsumer() {
  if (typeof RequestPlan === 'undefined') {
    throw new Error('Install the producer library as RequestPlan before running.');
  }
  if (typeof RequestPlan.getRelease !== 'function' || typeof RequestPlan.planRequests !== 'function') {
    throw new Error('The installed RequestPlan library does not expose the required public functions.');
  }
  const release = RequestPlan.getRelease();
  if (release !== EXPECTED_LIBRARY_RELEASE_) {
    throw new Error('Library policy label mismatch. Review the installed version and expected release before running.');
  }

  const requests = sampleRequests_();
  const plan = RequestPlan.planRequests(requests);
  validatePlan_(plan);
  Logger.log(JSON.stringify({ release, plan }));
}

/** Returns fresh fictional input on every run; no spreadsheet is read. */
function sampleRequests_() {
  return [
    { id: 'REQ-001', status: 'Ready', points: 3 },
    { id: 'REQ-002', status: 'Hold', points: 2 },
    { id: 'REQ-003', status: 'Ready', points: 5 },
  ];
}

/**
 * Checks the consumed result contract without reimplementing request planning.
 * @param {{requestCount: number, readyCount: number, workPoints: number, coordinationPoints: number, plannedPoints: number}} plan The returned plan.
 */
function validatePlan_(plan) {
  if (!plan || typeof plan !== 'object' || Array.isArray(plan)) {
    throw new Error('The library did not return a plan object.');
  }
  const counts = [plan.requestCount, plan.readyCount, plan.workPoints, plan.coordinationPoints, plan.plannedPoints];
  if (!counts.every((value) => Number.isInteger(value) && value >= 0) ||
      plan.requestCount > 100 || plan.readyCount > plan.requestCount ||
      plan.plannedPoints !== plan.workPoints + plan.coordinationPoints) {
    throw new Error('The library returned an incompatible plan. Review the selected version before using the result.');
  }
}

appsscript.json

Download file
appsscript.json
{
  "timeZone": "Etc/UTC",
  "dependencies": {
    "libraries": []
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": []
}