Going Stellar

Home/Methods

Engineering · Apps Script

How to deploy Google Apps Script code through the API instead of copy-paste

Google Apps Script code can be read and written through the Apps Script API, so a script no longer has to be pasted into the editor by hand. Our deploy tool runs as a dry run by default, backs up the current project before every write, re-sends every file because the API replaces the whole project, and verifies the result twice: by reading the code back and by checking what the script wrote.

Updated

What deploying Apps Script through the API means

You can replace copy-paste into the Google Apps Script editor with three calls to the Apps Script API: read the project with projects.getContent, change the files locally, and write them back with projects.updateContent. Done safely, every write is preceded by a dry run and a backup, re-sends all of the project's files, and ends with a read-back that proves the server now holds the code you meant to send.

A Google Apps Script project is a set of files: script files, of type SERVER_JS, and a manifest called appsscript, of type JSON. A project is either standalone or container-bound, meaning attached to one Sheet, Doc or Form. Our scripts are mostly container-bound: they build daily reporting rows in business spreadsheets, and each fix used to mean opening the editor, finding the function and pasting over it, with no backup and no record of what changed.

The one-time setup

Our tool authenticates as a service account, a non-human Google identity with its own key, using domain-wide delegation, a Workspace setting that lets that service account act as a named user of the domain. Three things must be in place. First, in the Workspace admin console, domain-wide delegation for the service account with the script.projects scope. Second, the per-user setting Google Apps Script API switched on in the Apps Script settings of the account that owns the scripts. Third, the Apps Script API enabled in the cloud project that holds the service account.

Reading and writing are not symmetrical. getContent works for the plain service account when the script is shared with it. updateContent fails with 403 User has not enabled the Apps Script API unless the call impersonates an owner who has the per-user setting on. The “user” in that message is the impersonated account, not the service account, which is why the error is confusing the first time. Delegation also takes a few minutes to propagate after you save it; until then the error is unauthorized_client.

For our Google Workspace add-ons we use clasp, Google's command-line tool for Apps Script, from a local folder under version control. The API tool described here is for scripts that live inside business spreadsheets owned by a Workspace account, where there is no local folder to push from.

Step 1: back up, then edit surgically

1. Find the script ID. Standalone scripts can be listed through Drive. A container-bound script does not show up there; its ID is in the editor under Project Settings.

2. Read and save the whole project. The get command writes every file of the project to a timestamped JSON backup before anything else happens. That file is the rollback.

3. Edit the backed-up source, not a retyped copy. We change code with exact string replacements and assert how many times each pattern matched, instead of rewriting the file. A syntax check follows: copy the file with a .js extension and run node --check on it. Formula strings inside the code need care: a spreadsheet with a non-US locale expects ; as the argument separator, so formulas must not be reformatted on autopilot.

Step 2: lint for the bugs that already broke production

Before every push, a linter checks the new source for the 7 mistakes that have already produced wrong numbers in our reports. Two are errors and block the push. getSheetById does not exist in Apps Script, and its use was the whole cause of a script that “writes nothing”; the method that exists is getSheetByGid. The second error is a bounded source range, such as a range that ends at the last row counted when the script runs. Our daily row is created before that day's data is synchronised into the source tabs, so new data lands past the frozen limit and every metric reads zero. An open range, with no end row, picks the data up as it arrives.

Five more rules are warnings: SpreadsheetApp.flush() on a large sheet, ARRAYFORMULA over whole columns, LET combined with array functions (it works on scalars only, and with arrays it returned zero), setValue called inside a loop instead of one setValues, and reading from the sheet inside a loop. The linter is not general advice; each rule is a bug we shipped.

Step 3: dry run, then write the whole project

push is a dry run unless given --apply: it shows which files would change and runs the linter. The important detail is that updateContent replaces the entire project. A raw call that sends only the changed script file deletes the manifest and every other file. Our tool therefore re-sends every existing file unchanged and swaps only the files named on the command line.

The same rules apply to the other write commands. create makes a new project, standalone or bound to a spreadsheet, and is also a dry run by default. There is no delete in the Apps Script API, so trash moves the script file to the Drive bin, where it can be restored for about 30 days; it refuses any file that is not an Apps Script project, so it cannot bin a spreadsheet by mistake.

Step 4: verify by reading back, then by effect

After writing, the tool reads the project back and compares it with what it sent. A successful API call proves that a transfer happened, not what was transferred. We learned that on the clasp side, when two add-on versions were published with the wrong files because a parallel workflow was swapping files in the same folder at the moment of the push; the push reported success both times.

Reading the code back does not prove that the script works, and the API cannot run it for us. We tried: scripts.run, processes.list and deployments.list all returned 403 with our delegation. Running remotely would require linking each production script to the service account's cloud project and adding an API-executable deployment to every one of them, which is too invasive. So the last check is by effect: after the trigger runs, a verify command reads the latest rows of the target tab and fails if a recent row has every metric at zero (the exact signature of the bounded-range bug), if any cell holds a formula error, or if bounded ranges remain in live formulas.

verify alarms only on a row that is entirely zero, never on a single zero metric. A brand whose advertising on one channel is paused on purpose would otherwise raise a false alarm every day, and an alarm that fires every day is soon ignored.

The traps we hit

Slow scripts were slow because of the sheet, not the code. The report tab held about 9,400 rows with about 15 live formulas each, around 140k formulas recalculating together. Converting rows older than 3 days into plain values at the end of each run fixed the speed.

Fixes to an append-only script do not repair history. One script appends a new row each day and never touches older rows, so a formula fix only affects rows created after the deploy; old rows keep the old values until they are rewritten deliberately.

Matching on an exact name fails silently. When a data feed renamed a store, every sum keyed on the old exact name returned zero without an error. Matching on a prefix or a case-insensitive pattern catches both the old and the new name.

A number inflated only in the dashboard is a dashboard bug. A single-day average fed a calculated field in our dashboard tool and inflated one figure about 3 times, while the sheet itself was correct. We first emptied sheet tabs looking for double counting that did not exist.

Where we were wrong, and the limits

We trusted the editor's “saved” and the API's success response as proof, and both times the proof was elsewhere: in the code the server holds and in the rows the script writes. We also assumed that remote execution would be a configuration detail; it turned out to require changing every production script, so we verify by effect instead.

The method has limits. Verification by effect only happens after the next trigger run, so a broken deploy can live until then; the backup makes the rollback quick, not instant. Domain-wide delegation is a powerful grant and belongs only to a service account whose key stays in a secrets vault. And the linter catches only the bugs we have already met.

Checklist

1. Set up delegation, the owner's per-user API setting and the cloud API once. 2. Back up the full project before every write. 3. Edit with exact replacements and a syntax check. 4. Lint for the bugs that already broke production. 5. Dry run first; write with --apply, re-sending every file. 6. Read the code back and compare. 7. After the next run, verify the rows the script wrote.

Questions and answers

Can I update Google Apps Script code without the editor?

Yes. The Apps Script API lets a program read a project with projects.getContent and write it with projects.updateContent. You need the API enabled in a cloud project, and for writes, an identity acting as an owner who has switched on the Google Apps Script API in their Apps Script settings. We use a service account with domain-wide delegation so the same tool can write to any script owned by a domain account.

Why does updateContent fail with User has not enabled the Apps Script API?

The error refers to the account the call acts as. With a service account, getContent can work while updateContent fails, because writing requires impersonating an owner whose per-user Google Apps Script API setting is on. Switch the setting on for that owner, make sure domain-wide delegation includes the script.projects scope, and allow a few minutes for delegation to propagate.

Does Apps Script updateContent delete files I do not send?

Yes. projects.updateContent replaces the whole project with the files in the request, so sending only one script file removes the manifest and every other file. Read the project first, keep every file you are not changing exactly as it was, swap only the files you intend to change, and send the complete set back. Keep the read result as a backup.

How do I verify an Apps Script deploy if I cannot run the script remotely?

Verify twice. First read the project back and compare its files with what you sent. Then verify by effect: after the trigger runs, read the latest rows the script wrote and fail if a recent row has every metric at zero or contains formula errors. With our delegation, scripts.run, processes.list and deployments.list all returned 403, so the effect was the only proof available.

How do I delete an Apps Script project through the API?

The Apps Script API has no delete method. Move the script file to the Drive bin with the Drive API instead, which needs a Drive scope in your delegation. The file can be restored for about 30 days. Check the file's type first and refuse anything that is not an Apps Script project, so a spreadsheet or document is never binned by mistake.

How to cite this

Going Stellar (2026). How to deploy Google Apps Script code through the API instead of copy-paste. GOING STELLAR S.R.L..
https://goingstellar.ro/methods/deploy-google-apps-script-via-api/

Read as Markdown

All methods

Contact

Tell us what is not working

Write in English or Romanian. A person reads it — usually the same person who would do the work — and answers within one business day.