The 6 deadly sins of Google Apps Script Add-on development

So you want to build an add-on for Google Docs, Sheets, or Slides, but you’re worried about ending up on the highway to development hell? You’ve come to the right place, then. As someone who has been down that road before, I’d like to share some of the mistakes, missteps, and blunders I’ve either fallen victim to or narrowly avoided, what I’ve learned from them, and how you can chart a course through the Google Apps Script add-on development process that bypasses them completely—without having to program in Vigil. In particular, I’ve focused on things you might consider doing to save time or effort that will end up costing you dearly. I realize that there are traditionally seven deadly sins, but I couldn’t think of a seventh that was sufficiently sinful, so we’ll have to go with six. Let’s get started:

#1: Coding your Google Apps Script add-on in the built-in script editor

It’s always a chore doing the prep work necessary to be effective in a new environment when all you really want to do is jump in with both feet and start experimenting with APIs, and starting a new Google Apps Script add-on is no exception. That’s what makes the built-in script editor so tempting: you don’t have to set anything up or read any documentation. You can just click “Tools > Script editor” and then jump in and start coding immediately. However, like most temptations, it comes with a heavy cost later on. The built-in script editor offers minimal source control options, bare-bones search functionality, and a text editor that will get the job done but lacks quality-of-life features like automatic closing parenthesis insertion and simultaneous multiple line indentation. It’s certainly enough to get small projects working, but once you are dealing with more than a handful of files, you will eventually reach your breaking point, and that’s when the second temptation will kick in.

The built-in editor for Google Apps Script add-ons
It’s a trap!

“Why not just do my work in <IDE of choice> and then copy-paste it to the script editor afterward?” you’ll ask yourself. Well, I’ll tell you why: the fact that you’re asking yourself this means that you’re dealing with a nontrivial degree of complexity. Otherwise, you could tough it out for 30 more minutes and be done with it. That complexity means you should be using professional-grade source control and good engineering practices instead of hacking away and hoping for the best while trying to remember which files have, and have not, been copy-pasted recently. Luckily, you can avoid this entire mess—and keep using your favorite IDE—by using the Google Drive REST API to import and export your code instead. It’s a lot like using file-by-file copy-paste, except it makes the computer do all the boring and repetitive parts that are vulnerable to human error. In fact, if you set up git hooks that upload your changes automatically, it will be no different than a typical feature branch workflow (don’t forget to squash). So make sure you set this up at the very beginning. I promise you won’t regret it.

#2: Hardcoding app-specific strings and API calls

Unless your Google Apps Script add-on’s purpose is extremely specific to Sheets (for example), you will eventually want to port it to Docs. Or even if it will never be ported to another G Suite app, you may want to release translations of it in Spanish, French, German, and Chinese. If you’ve hardcoded every string relating to spreadsheets, you’ll have to hunt them all down to hand them over to your translator and then manually put them back in. If you’ve hardcoded every API call to SpreadsheetApp.getThing().getOtherThing().getWidth(), then you’ll have to replace it with DocumentApp.getDifferentThing().getWidth() all throughout your codebase. Or if you update the UI for one of multiple add-ons that are basically the same, you will have to move those changes over very carefully to make sure you don’t break anything in the other add-ons—which you probably weren’t thinking about very hard when making the updated UI.

Comparison of old UI and new UI of the Lucidchart Google Apps Script add-ons
Original UI in Docs/Sheets/Slides (left); redesigned UI in Docs/Sheets/Slides (right)

On the other hand, if you had started with a wrapper object that abstracted away which specific G Suite app the add-on was for and an i18n object to hold all user-facing strings, then porting or translating your add-on would be much easier, faster, and less error-prone. I ended up calling my wrapper object IntegratedApp, and all it really did was provide a common interface for the rest of my code to use without caring about which app it was for. Instead of making the above SpreadsheetApp and DocumentApp calls, I would just call IntegratedApp.getThingWidth() and then define what that meant within the IntegratedApp wrapper object to match the application using it. This way, all code could be shared between add-ons, with the sole exceptions being a configuration file and the file defining IntegratedApp. Because it’s easiest to develop using a wrapper if you start doing so from the beginning, I recommend doing just that.

Diagram of IntegratedApp's relationship to DocumentApp, SpreadsheetApp, and SlidesApp within the Lucidchart Google Apps Script add-on
UML class diagram showing the relationship between IntegratedApp and Docs/Sheets/Slides

#3: Only testing in AuthMode.LIMITED

This one is easy to forget because most of your work in a Google Apps Script add-on will be based on the functionality that is only available after the add-on has been enabled, which naturally means that most of the time you’ll be in AuthMode.LIMITED. Unfortunately, most of your new users will not be.

Diagram of authorization mode determined by the Google Apps Script add-on lifecycle
Determining your authorization mode using the Google Apps Script Add-on lifecycle

Check out the Google Apps Script add-on authorization lifecycle, and you’ll see that until a new user enables your app—even if it’s already installed—your onOpen() script will be running in AuthMode.NONE. So if your onOpen() does something that can only be done in AuthMode.LIMITED or higher, you’ll spend the first day of your Google Apps Script add-on’s launch scrambling to rewrite every violating function so your users can actually use your add-on. In my case, I was calling getActivePresentation() in order to set up some function bindings for my IntegratedApp object. Since I wasn’t actually trying to do anything to the document before I was allowed to, it was simple to wrap that call in another function instead of setting the bound function directly as a property of the wrapper, ensuring it would be evaluated after the add-on had been enabled instead of immediately upon onOpen’s execution.

BadIntegratedApp = {
    getPageWidth: SlidesApp.getActivePresentation().getPageWidth
        .bind(SlidesApp.getActivePresentation())
};

This executes SlidesApp.getActivePresentation() upon load, so it will fail immediately in AuthMode.NONE

GoodIntegratedApp = {
    getPageWidth: function() {
        return SlidesApp.getActivePresentation().getPageWidth();
    }
};

This defers executing SlidesApp.getActivePresentation() until IntegratedApp.getPageWidth() is called
Fortunately, it’s easy enough to make sure you catch this problem before release by putting “test for brand-new users” in your QA specification and (even better) testing while you develop by using “Run > Test as add-on” in the script editor to create a test case that starts in AuthMode.NONE.

#4: Not separating your HTML/JS/CSS

Google Apps Script add-ons only support two file types: .gs and .html. The .gs files are written in Google Apps Script (which is pretty close to Javascript with the name filed off) and run on the backend. These aren’t usually the source of this problem. As you might guess, the .html files are written in HTML and make up your add-on’s front-end. Seeing that you’re coding directly in HTML may bring you back to the days of using HomeSite to write DHTML and lead you to bundle related HTML, CSS, and JS together in one file. After all, you don’t have the option to include .js and .css files, right? Technically yes—but practically, there’s a way around this limitation.

Although you can’t include .js and .css files directly, there’s nothing stopping you from creating .html files that function as if they are .js and .css files. Wrap your JS in a <script> tag, and voilà! You’ve got valid HTML. Wrap your CSS in a <style> tag, and you’ve got valid HTML. You can even use .js.html and .css.html in your filenames to make it perfectly clear that while this is an “HTML” file, it’s really a JS file. Then in your actual .html file, you can include your JS and CSS by using printing scriptlets like the following:

<?!=HtmlService.createHtmlOutputFromFile('loginForm.js').getContent()?>
<?!=HtmlService.createHtmlOutputFromFile('sidebar.css').getContent()?>

The ‘.html’ in the file name is implicit—no need to include it here
Note that there are XSS security concerns to take into account when you are using force-printing scriptlets (which the above are), so be sure to read up on them before using them. In many cases, regular printing scriptlets will serve you just fine. Understanding printing scriptlets and using that knowledge to separate your styles, DOM, and business logic only takes a little bit of reading and a little bit of work, but when you end up having to restyle your add-on, you’ll be glad you have a clean division between your styles, scripts, and HTML.

#5: Failing to understand environment restrictions at design time

Writing an add-on for G Suite is very similar to normal web development, but there are some things you could do on a normal web page that are just not possible in an add-on. It’s important that you understand these limitations before beginning development, and it’s just as important to convey them to your UI/UX designer before they start making mocks. It’s easy to think (for example) that you can put a contextually-captioned button in the title bar of your sidebar add-on, but it turns out that you can’t. If your mocks depended on that functionality being available, it’s back to the drawing board. While I cannot give an exhaustive list here of cans and cannots, I will explain a few, and I will note that the dialog guide and the add-on style guide will probably be useful to your designer.

Firstly, you cannot violate the borders of the iframe sandbox, and all sandboxing is done in iframe mode. Try as you might, you will not be able to throw a div into the middle of the page to make a beautiful image selection modal dialog. You have to instead use the dialog options that the UI API provides.

Secondly, paying close attention to the UI API will reveal that all calls to it come from your .gs files (which run on Google Apps Script servers), which means that a modal dialog created by your sidebar is actually created by your .gs backend script messaging Google’s frontend UI service in the browser. So if you pass in a JSON object full of functions that can affect the sidebar as a creation parameter of the modal, you will find that they will not be able to affect the sidebar through closure. This is because in order to pass that JSON object to the modal, you serialize it and send it to Google’s servers in a request, and then it gets deserialized on its way back to Google’s UI service, which uses the deserialized JSON to create the modal. The serialization/deserialization destroys any notion of closure on the JSON’s functions. You can use printing scriptlets to pass information from the sidebar to the dialog at its time of creation, but after that data flow between the two is limited to dialog events the backend will be listening for and backend functions explicitly invoked from the dialog using google.script.run.

Diagram showing where serialization destroys closure when sending a JSON object between the sidebar and a dialog of a Google Apps Script add-on
The function we want to use to affect the sidebar via closure doesn’t make it past JSON.stringify()

Finally, new Google Apps Script add-ons must comply with Google’s Material Design look and feel. You won’t be completely prevented from developing your add-on if you ignore this requirement, but you’ll have a bumpy road to travel in order to get it approved for release. Even if (as in my case) the Docs and Sheets versions of an add-on use an older, non-Material UI, your new add-on still has to use Material. With that said, the resources for Material Design are pretty nice to work with, so this particular restriction is not too painful as long as you know about it from the beginning.

#6: Delaying marketing materials until after submission for review

As engineers, it’s easy to dismiss marketing as “fluffy ‘people stuff’” and assume that your marketing department can spin up anything you need on short notice. Or, if you’re going it alone, it’s easy to think, “once I have the add-on working, I’ll submit it to get the functionality approved, and I’ll write up my promotional text while I wait for approval.” However, as you will find at 11:30pm on your submission deadline date, you can’t actually submit your add-on for approval without creating its webstore entry, which means submitting marketing materials. You can throw in dummy blank thumbnails and Lorem Ipsum text to make the forms submit, but you will have to get your add-on reapproved once you’ve put real images and text in, and all the while, your add-on will be not be in the webstore (if you’re lucky), or it will be in the webstore with nonsense text and blank images (if you aren’t so lucky). Save yourself the headache and get your media assets prepared long before your deadline.

Google Apps Script add-on webstore entry form
A small part of the webstore draft form you must complete to submit your Google Apps Script add-on

Conclusion

You may have noticed that the common theme running through my recommendations is “prepare in advance.” This was not an accident. Easily the biggest difficulties in software development come from not thinking things through until a serious problem has reared its head, and add-on development is no exception. But if you are patient and do the unglamorous legwork first, you will find that writing Google Apps Script add-ons is actually quite enjoyable, and a well-prepared engineer has nothing to fear from them.

As for me, surviving the mistakes and near-mistakes I encountered over the course of developing my Google Apps Script add-on was a rollercoaster, but completely worth it. I was able to be a launch partner for the announcement of Google Slides add-ons, and since then, our add-on has racked up almost 20,000 individual installs.

18 Comments

  1. Hey Tim,
    I have a question about the first sin you mention:
    Coding your Google Apps Script add-on in the built-in script editor

    I have been coding in the built-in editor and prefer it. Admittedly it may be due to the relative simplicity of the projects I’ve been working on. A while back, I considered editing outside of the provided editor, but found that I preferred the auto-completion of library-specific methods over the auto completion of parentheses etc. Have you found plugins for either atom, sublime, etc. that add that functionality (e.g., type “SpreadsheetApp.get…” and have the options listed?

    Additionally, I enjoy coding in the GAS script editor for the ease of debugging on the fly (with recognition of these methods, inherent permissions granted, etc.) and for the ability to use a sheet for testing output.

    I imagine this mostly comes down to the difference in project style—most of what I do is custom work, mostly container-bound, and is heavily reliant on service-specific methods, as opposed to general, stand-alone add-on development. Nevertheless, if you have thoughts or suggestions, I would be happy to hear them.

    Thanks!

  2. Tim WinchesterJanuary 22, 2018 at 6:55 pm

    Hi Andy,
    Great question!

    I agree that it comes down to the type of project you’re working on. Smaller, simpler projects and (especially) custom container-bound scripts you will use temporarily before discarding tend not to run into the sort of problems that build up over time, and container-bound scripts can’t be automatically uploaded anyway*. I haven’t found any apps script library plugins for Sublime, but because all my app-specific code was consolidated into my IntegratedApp implementation, there was only one file per add-on where library-specific auto-completion of that kind would have helped.

    Admittedly, while I did find the lack of paren-completion and limited search features annoying, what it really came down to was version control. I wanted to be able to use git, and I wasn’t able to without copy-pasting. So, confession time: copy-pasting back and forth between editors is the real sin, but “Coding your Google Apps Script add-on in the built-in script editor, hitting your breaking point, and then making a habit of copy-pasting between your editor and the built-in editor” is much too long to be a workable section heading, and I thought that if A inevitably leads to B, and you want to prevent B, you need to warn against A. Although I mentioned that the built-in editor is “certainly enough to get small projects working” in passing, I admittedly had moderate-to-complex standalone add-ons in mind when I wrote this article, and didn’t consider the group of people that will never hit their breaking point with regards to the built-in editor. So, for the record: in your use case, you’re probably doing it right.

    *If this is wrong and you know how to pull off uploading a container-bound script, I am completely ready to look dumb here in exchange for you telling me how to do it

  3. Very useful information, and I totally agree with #1. Having started in the online editor I soon realized that “manage versions” has nothing to do with versioning :-/ Fortunately there’s now a tool called ‘clasp’ – no autocomplete for the Google APIs, but worth it to be able to use git and a proper IDE!

  4. Tim,

    What do you use for packaging, or at least project directory structure? I’m sure there’s a seventh deadly sin there.

    – {another} Tim

  5. Tim WinchesterMarch 21, 2018 at 12:16 pm

    Hey Another Tim,

    You’re gonna love this one.

    As you know, you can’t have subdirectories in Google Apps Script projects. What you might not know is that you can use commas in filenames there. Thus, what is “app/controller/mycoolscript.gs” locally can be transformed to “app,controller,mycoolscript.gs” for upload, which allows you to see (essentially) the same file structure at a glance if you have to look in the script editor. Is it ugly? Yes. Is it dumb? Also yes. But it hasn’t caused any problems for me thus far, so I’m inclined to say it’s a workable strategy.

    Regarding packaging, I’ve entertained the idea of using the Closure compiler, but haven’t actually started using it for Google Apps Script.

  6. Chris HundhausenMarch 29, 2018 at 1:02 pm

    Hey Tim,

    I really enjoyed your article. Having developed with Google Apps Scripts full-time over the past 7 months, I think you’ve captured many important points.

    I’m still using the built-in editor despite the fact that my project has grown to over 20K lines of code. I’m looking to transition to a more industrial-strength environment, and I just came upon Clasp (https://github.com/google/clasp). It appears that there is even a way to get autocomplete to work in industrial-strength IDEs such as Visual Studio, although I don’t know if the Google API libraries are auto-completeable. It also appears that Clasp can work in tandem with Git, although I’m assuming that you still need to host your code repository in the cloud somewhere; it’s not hosted by Google if I understand correctly.

    Do you or any of your readers have experience with using Clasp in tandem with Git? I’d be interested in your advice and insights on this topic.

  7. Hi Tim,

    Great article, it comprehensively goes in details through most of Apps Script issues (and frustration) we’ve had !

    However as of today (05 avril 2018) there are mitigation possible for several pain-points:
    – working with container-bound is now possible
    – GAS auto-completion in your local development environnent
    – coherent Folder structure in your GAS editor cloud UI

    # working with container-bound:
    With the new GAS API, and the Google-released CLASP command-line tool, it’s now possible to have complete logical access to every of your AppsScript projects, container-bound or not !
    -> https://developers.google.com/apps-script/api/

    # GAS auto-completion in your IDE:
    There are several solutions available though I failed to list them just now… We may see support coming with CLASP as Google seems to slowly understand that it is a needed item.
    In the meantime, you can add GAS support with this project:
    https://github.com/JeanRemiDelteil/gas-lib

    # Folder structure in GAS editor:
    Well, it’s a custom solution, not in the native editor, but if that is okay with you, you can use the Chrome extension AppsScriptColor:
    https://chrome.google.com/webstore/detail/appsscript-color/ciggahcpieccaejjdpkllokejakhkome
    It adds colors in the UI, and also a Folder structure when you use folder/subFolder/file_name pattern.
    On this note: CLASP CLI automatically use this pattern when you push local files inside folders.

  8. Tim WinchesterApril 5, 2018 at 5:21 pm

    Hi Chris,

    Clasp looks great, but I haven’t had a chance to use it: I’ve been drawn into other projects and am actually handing over all Google Apps Script projects to a newly-forming team. Luckily, it will be lead by one of my current team members, Kamaron Peterson, whose high level of competence I can personally attest to. He hasn’t promised to write a post about Clasp yet, but I’ll see if I can tempt him with baked goods.

  9. Tim WinchesterApril 5, 2018 at 5:35 pm

    Hi Jean-Rémi,

    This does look fantastic, especially access to container-bound scripts. Although I won’t be writing in Google Apps Script in the foreseeable future, I applaud the Clasp team for putting it together, and I wish it had been available last year!

  10. […] The built-in script editor offers minimal source control options, bare-bones search functionality, and a text editor that will get the job done but lacks quality-of-life features like automatic closing parenthesis insertion and simultaneous multiple line indentation. It’s certainly enough to get small projects working, but once you are dealing with more than a handful of files, you will eventually reach your breaking point, and that’s when the second temptation will kick in. <Extracted from https://www.lucidchart.com/techblog/2017/12/07/6-deadly-sins-google-apps-script-add-on/> […]

  11. From my own experience: Especially when your app also involves an own server: Try to minimize the amount of Apps Script code you write. You can do the user interface in an IFRAME coming from your own server. This lets you use all standard frameworks (like react.js), simplifies development process (you just change your server) and removes Googles approval from the change process.

  12. Tim WinchesterApril 25, 2018 at 10:50 am

    Hi Ruediger,

    This is a great point, and I completely agree. It’s not just specific to Apps Script either–most of the time when you’re writing an integration, any code written for your target environment isn’t reusable, but iframed or otherwise-imported code can be. You won’t necessarily reap the benefits right away (although you probably will, if only from frameworks), but you will definitely be glad you did in the long term. Just be on the lookout for any places where your performance may suffer as a result (this is a problem I ran into when developing Lucidchart for Quip).

  13. Hello Tim,
    As you developped an addon with 20k install, did you have any problems with the limitations (Quotas) forced by Google?
    Thank you

  14. Beena KushwahaApril 12, 2019 at 12:09 am

    Hello Tim,

    I have a confusion about importxml function. When Google bot crawl the page, it cached the data. I have written app script to automate meta tags testing. So , on every script execution it will provide updated data not cached data. To achieve this, my script call same url with minor tweak(with random no.). Will there be any impcat on SEO of that particular url?

    Sample code.
    var formula1 = “//meta[@*[contains(translate(., ‘ABCDEFGHIJKLMNOPQRSTUVWXYZ’, ‘abcdefghijklmnopqrstuvwxyz’),’description’)]]/@content”;

    var descommand = ‘IMPORTXML(“‘ + cellreference + ‘&utm_meta?’ + queryString + ‘”,”‘+ formula1 +'”)’;

  15. You actually make it seem so easy with your presentation but I find this topic
    to be really something that I think I would never understand.

    It seems too complex and extremely broad for me.
    I am looking forward to your next post, I will try to get the hang of it!

  16. Point number one is now a bit more debatable with the December 7, 2020 release of the new Google App Scripts IDE. Still a great post!

  17. I tried to separate the CSS by putting … in GAS HTML file and including as you suggested and which I saw elsewhere. The above text appears at the top of the Dialog and is not included so no formatting. Is there something strange about showModalDialog?

  18. Michael R. WolfDecember 15, 2022 at 6:50 pm

    I loved point #1. I was looking to solve this exact problem. But I’m missing something…. a path name. It’s nice that there’s an API to code some other app, but my tools (IntelliJ and git(1)) need path names. I could use the API to build a file system emulator, but that sounds a lot more difficult than it seems to have been for you.

    What am I missing?

Your email address will not be published.