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, pin, upgrade, and roll back its version, and explain how saved code, numbered versions, development mode, and sharing differ.

Chapter navigation

Write a request-planning function, save it as a numbered library version, and call it from a separate Apps Script project. With the supplied fictional data, the first policy plans 10 points of work and a later policy plans 12. The consuming project 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. In this chapter the producer project owns the calculation and its releases. The consumer project owns its input and chooses which release to call. Keep the two projects separate. If you copy the calculation into the consumer, you cannot see how versions and access work.

You need a Google account that can create and run private Apps Script projects. Use two new standalone projects, not an automation that people already rely on. 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 a line in the execution log.

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. The two Ready requests contribute eight work points and two coordination points, for a plan of ten. A Hold request must still be a valid record, but it 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 integer points from zero through 100. Extra fields are ignored. An empty list is valid. Missing input, repeated IDs, points written as text, unknown statuses, and malformed records are invalid. The function validates every record and either returns a complete result or throws an error. It never skips invalid requests to return a smaller total.

The result is a new object with requestCount, readyCount, workPoints, coordinationPoints, and plannedPoints. The function does not change the input array or its records. This is a pure calculation. For the same accepted input and the same library policy, it returns the same numbers without reading a sheet, using the clock, or storing a counter. Consumers can reuse the calculation regardless of which files, accounts, or permissions they have.

getRelease() returns a label such as request-plan-1. You write this label in the source yourself. It is not the Apps Script version number. The consumer checks this label to reject an unexpected policy. Any code can return the label, so it does not prove which code ran. The producer maintainer must change the label whenever the policy changes.

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 do not put it in a folder whose sharing would give people access you did not intend.

  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, because 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 Apps Script 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 plain JavaScript, and the preview adds only Logger.log. This source needs no permission to read Drive, Sheets, or Gmail or to make external HTTP requests. View the scopes under Overview > Project OAuth Scopes. If a copied project or dependency asks for unexpected access, inspect its source and manifest instead of approving a broader permission to get past the prompt. Signing in to 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, and it does not prove that a separate consumer can load 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, which include ordinary function declarations and globals declared with var. Apps Script excludes a function whose name ends with an underscore from the library API. That is why validateRequest_ and previewLibrary_ can be called inside the producer but not through the consumer's library identifier.

A top-level const or let does not create the same enumerable global property as a var declaration. Do not rewrite the public functions as top-level const arrow functions and assume the library API is unchanged because the editor can still run them. The editor's function list and library export visibility are different mechanisms. The private constants in this producer are implementation details. Consumers get the release label through a function instead of reading a global variable.

The JSDoc comments describe parameters, return fields, and failures. Apps Script uses them for library autocompletion 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 that reference documentation.

A trailing underscore prevents calls through the library identifier. Library users need read access to the producer source, so they can still read the helper. Do not put credentials or confidential data in a library and expect the naming convention to hide them.

Save a numbered library release

Head is the producer's latest saved editor code. A version is a snapshot that cannot change, identified by a number assigned within that script project. A deployment makes a version available for use. All three are distinct from the request-plan-1 label in your source. Google's versions guide explains what a version contains, and 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, but use the number you see. Call this recorded number N1 in your notes, and use it wherever this lesson says N1.

  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, an OAuth client ID, or a release label cannot identify an Apps Script library.

In your release notes, record these together: the producer Script ID, N1, its policy label, and the source you inspected. To find out what a numbered version contains, inspect its source under Project History and run the real consumer with that version selected. A local copy of the source does not show which code Apps Script runs for that version 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 sharing is needed for the basic exercise.

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 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. Knowing a Script ID does not grant access.

Give producer Editor access only to maintainers. Editors can change the source and create new versions, which an ordinary consumer does not need. Viewer access is enough to use numbered releases. The library-specific head testing instructions require editor access, which matters when you try development mode later.

A pinned version still stops working if the producer is deleted or your access is removed. Assign a maintainer, and plan how you will keep the producer and its old versions. The consumer manifest only points to the library and does not contain a copy of it.

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 version number do not exist until you create them. Before installation, runLibraryConsumer stops with Install the producer library as RequestPlan before running. The RequestPlan name exists only after you install the library.

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 that the returned project is yours and not 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

The library manifest reference documents these fields. It also allows the value stable for version, but this exercise uses an explicit number so that the manifest records the intended snapshot. Keep the rest of the manifest, including the empty scopes array. Once the dependency is configured, replacing the manifest with the consumer base manifest would remove the installation.

Select runLibraryConsumer, click Run, and inspect Execution log. With the original sample data 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, because the library keeps no running total.

RequestPlan.planRequests(requests) calls the library through the installed identifier. The name RequestPlan comes from the Identifier you set during installation. The consumer makes one call for the release label and one planning call. validatePlan_ checks the shape of the returned fields before logging them, but it does not repeat the library's 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 you see a platform error instead of the Install the producer library message from the typeof RequestPlan guard. Fix the dependency or the permissions. Do not add a 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, and 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. You changed only the consumer, so you do not need a new producer version. Editing the producer preview's records would not change this consumer's input either, because each project has its own array.

Next, with the original statuses restored, change only the second record's points: 2 to points: '2'. This is still valid JavaScript, but the library accepts only numeric points. 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 the numeric 2, save, and rerun to recover.

You can also make sampleRequests_ return an empty array. The result is a valid zero-request plan with all five fields at zero. Restore the supplied records before the version exercise so that input changes do not hide 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 the numbers change, so each consumer still needs to decide when to adopt it.

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 sample data. 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 already shows the new policy or a label mismatch, 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. Do not change the expected label to make the error go away. The error means the consumer is not running the version you expected.

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 does not change. Updating the library deployment to a new version does not change any consumer's dependencies.libraries selection.

The consumer maintainer owns the upgrade. This practice project has no triggers or external callers, so stop manual runs while you change the dependency and the expected label together. For a real scheduled or deployed consumer, its owner must plan how to pause or release that change without running 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 with the original sample data. Expect:

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

If you change only the selected version and leave the old expected label, the consumer stops with a policy-label mismatch. The check is useful because the function signatures alone would not reveal the change in points. Still compare the actual numbers, because a matching label does not prove the result is correct.

Now roll back. In Libraries > RequestPlan, select the saved N1 number and click Save. Keep development mode off, restore EXPECTED_LIBRARY_RELEASE_ to request-plan-1, and run with the unchanged sample data. Expect ten points again. Choosing an existing snapshot needed no change to the producer's newer head and no new version.

Rollback affects this consumer's future calls. It does not update a different consumer, erase a log, or undo anything a library with broader permissions already did. Keep a record of consumers, owners, selected versions, and release behavior. Do not delete the producer or older versions, or remove producer access, while another project may still depend on them.

Use development mode only for a short experiment

Once the producer has at least one saved version, its owner or an editor can select HEAD (Development Mode) in the consumer's library settings. Google's library testing guide sets this access requirement. Do not grant someone Editor access so that an ordinary pinned consumer can run.

Selecting HEAD (Development Mode) sets developmentMode to true. The manifest reference says that this ignores version and uses the current library project code. While that flag is true, a version number left in the manifest is not a pin.

For the optional experiment:

  1. Keep the producer head on request-plan-2.

  2. In the practice consumer, select HEAD (Development Mode) and set its expected label to request-plan-2.

  3. Run with the original sample data. Expect twelve planned points.

  4. Temporarily change the producer's allowance from 2 to 1 and save.

  5. Rerun the HEAD consumer. The planned points should become ten, even though the release label still says request-plan-2.

  6. Restore the allowance to 2 immediately. Do not create a numbered release of the temporary mismatched source.

Development mode runs saved head code without a new numbered release for each edit. The experiment shows that each save changes head and that the release label does not prove which code ran.

Finish by selecting a numbered version, setting developmentMode false, and restoring the matching expected label. Check the result with the original sample data 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 does not run code as the library's owner. Here the consumer's manual run passes plain input to the library, and the library does not use the producer owner's access to any file. Before consumers adopt a library that starts using services, someone must review its effects, scopes, and resource ownership.

A library does not have its own copy of every resource. The resource-scoping table separates resources tied to the library from resources shared with the calling script. For example, the library's Script Properties are its own store, shared across all its consumers, while User Properties and logging follow the shared-resource rules.

If you move a helper that reads or writes properties into a library, do not assume it still reads and writes the same properties. This example avoids that problem by using no properties, cache, locks, active container, or triggers.

The library's code runs as whoever runs the consumer project, with the access that account granted to the consumer. Install only a library you trust. The library can read everything you pass it. A new release can add side effects even when its function names do not change. An underscore, the consumer's result-shape check, and an empty scopes list do not isolate the library's code. Review the producer source and release before installation, keep the consumer's permissions narrow, and treat a new service requirement 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 one array of limited size for one calculation instead of calling the library once per cell. For a single tiny helper in one project, maintaining library releases may be more work than it saves. A library 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 or build change, followed by uploading the emitted code
What still needs review?Public functions, 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 do-nothing object can stand in for the library configuration. A bundler must also produce code that Apps Script can run, without Node-only APIs or unresolved imports. The local-development workflow is a separate way to manage source. It does not change how Apps Script loads libraries.

Diagnose library errors

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 code 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 sample data to separate data errors from release errors.
An incompatible-plan errorStop using the result. Inspect the selected library's return fields and source. Do not fill 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 the current producer source, which may differ from the numbered version the consumer uses. Compare both when diagnosing a release problem.

Renaming the identifier is another change the consumer owns. As a short experiment, rename the installed identifier to WorkPlan, save, and run the unchanged code. It should fail to find RequestPlan. Restore the identifier and rerun. For a permanent rename, update every reference to the identifier, including the guard, the method checks, and the calls. Renaming the producer project does not change the identifier an existing consumer chose.

Nothing here retries automatically. Rerunning does not fix a calculation error. A version or access problem needs a correction. After you correct it, running the same sample data again repeats no action and builds up no state. Apps Script can still fail an execution or a log write, so a missing log does not mean the calculation did not run.

Leave the consumer on a pinned version

Restore the original consumer sample data, including the numeric 2 and the Hold status. Restore the identifier to RequestPlan if you renamed it. Select a numbered version with development mode off, set the matching expected label, and check the result once more. Keep both producer versions if you want to keep 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.

The example created no tokens, schedules, spreadsheet output, or persistent counters to clean up.

Next, read OAuth for separate accounts.

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": []
}