Cloud Apps Software

DATEDIF How to Calculate Difference Between 2 Dates

The DATEDIF function calculates the number of periods between two dates. The best way to remember this function is it calculates the DATE DIFference between two dates.

What is DATEDIF?

The DATEDIF function is a popular formula that has 3 parameters which are all required. If you [encounter any error]({{< relref “#datedif-errors-and-problems” >}}) when using this function check you have populated all three fields correctly.

The function reference is as follows:

=DATEDIF(start_date, end_date, unit) 

The start_date parameter should be the earliest date, the end_date being the latest date, and the unit is the value of the periods you want returned from the difference between those two dates.

Accepted values in Google Sheets for the unit value are:

Date Difference in Days

By setting the third parameter to the DATEDIF formula to D it means the resulting value returned will calculate the number of days between the two dates. Here are some examples:

11 June 20201 June 2020=DATEDIF(A1, B1, "D")0
21 June 20202 June 2020=DATEDIF(A2, B2, "D")1
31 June 202031 May 2020=DATEDIF(A3, B3, "D")#NUM!

Several things to note about these tests when using the DATEDIF function and the unit parameter as D:

  1. The dates are not inclusive. Notice the first row is 0, if it were inclusive it would have been 1.
  2. Should the end_date come before the start_date you will get an #NUM! error. Make sure your end_date occurs on or after the start_date.

Date Difference in Months

If you want to calculate the difference between two dates in months, then set the third parameter of the formula to "M", here are some interesting examples of what this looks like for certain values:

129 February 202029 February 2020=DATEDIF(A1, B1, "M")0
229 February 202028 March 2020=DATEDIF(A2, B2, "M")0
329 February 202029 March 2020=DATEDIF(A3, B3, "M")1
429 February 20201 March 2021=DATEDIF(A4, B4, "M")12
531 January 202029 February 2020=DATEDIF(A5, B5, "M")0
631 January 20201 March 2020=DATEDIF(A6, B6, "M")1

As can be seen from the table above, the day of the month needs to be either eclipsed or matched for the returning value to increment. In rows 5 and 6 especially we can see the days in the month of February 2020 would not have eclipsed the start_date of 31st January 2020, but when the end_date advances into the next month (1st March) the increment occurs.

Therefore, if your start_date is the end of a month containing 31 days it will not be until the 1st day of the following month, preceding a month containing only 28, 29 or 30 days, that the value will be incremented.

Date Difference in Years

If you want to calculate the difference between two dates as years then set the third parameter value to "Y". Here is what this would look like based on the following inputs:

129 February 202029 February 2020=DATEDIF(A1, B1, "Y")0
229 February 202028 February 2021=DATEDIF(A2, B2, "Y")0
328 February 202028 February 2021=DATEDIF(A3, B3, "Y")1
429 February 20201 March 2021=DATEDIF(A4, B4, "Y")1

As can be seen from the examples above, it’s not until the date and month of the start_date has been eclipsed or matched the value returned by the DATEDIF formula increments. This is very similar to when calculating the [date difference in months]({{< relref “#date-difference-in-months” >}}).

Date Difference in Days Irrespective of Months

If you need to calculate the difference in days between two dates irrespective of the difference between months then you’ll need to use "MD" as the value of the third parameter, as demonstrated below:

131 January 202031 January 2020=DATEDIF(A1, B1, "MD")0
231 January 202029 February 2020=DATEDIF(A2, B2, "MD")29
331 January 20201 March 2020=DATEDIF(A3, B3, "MD")-1
431 January 20202 March 2020=DATEDIF(A4, B4, "MD")0
531 January 202030 March 2020=DATEDIF(A5, B5, "MD")27
631 January 202031 March 2020=DATEDIF(A6, B6, "MD")0
731 January 20201 April 2020=DATEDIF(A7, B7, "MD")1

As previously noted with the [difference in days]({{< relref “#date-difference-in-days” >}}) we can see from the examples above the difference in days is not inclusive. However, there are some interesting results:

Row 3 shows a result of -1. Why? This implies there was a day short in the previous month of getting to the same date, and the current date of 1st March 2020 is 1 day short of the 31 days (29 days in February + 1 day in March = 30 days – 31 days = -1). This then puts the remaining values of that month (in March) out, until we hit the new month April.

Date Difference in Whole Months

If you want to calculate the difference between two dates in whole months you can set the third parameter to the DATEDIF formula to "YM".

131 January 202031 January 2020=DATEDIF(A1, B1, "YM")0
231 January 202029 February 2020=DATEDIF(A2, B2, "YM")0
331 January 20201 March 2020=DATEDIF(A3, B3, "YM")1
431 January 202031 March 2020=DATEDIF(A4, B4, "YM")2

The results from this result set above are fairly simple to understand and are consistent with what we already know about – the day of the month needs to be matched or eclipsed to increment the value.

Date Difference in Days Irrespective of Years

Similar to our previous example with [date differences in days irrespective of months]({{< relref “#date-differences-in-days-irrespective-of-months” >}}) we have a similar calculation but this time irrespective of years, here are some examples:

129 February 202029 February 2020=DATEDIF(A1, B1, "YD")0
229 February 202028 February 2021=DATEDIF(A2, B2, "YM")365
329 February 20201 March 2020=DATEDIF(A3, B3, "YM")0

Consistent with the days difference irrespective of months we see that where the start date isn’t matched by day of month and month, it calculates the difference the previous month fell short.

Benefits of DATEDIF Over Subtracting Two Dates

An objection to using the DATEDIF formula is that it achieves the same result when subtracting dates, which is certainly far simpler than trying to remember a formula, and it’s parameter values.

The biggest benefit of the DATEDIF formula is that it helps to calculate the difference between two dates by providing us with the type difference we need. Therefore, the DATEDIF formula is more versatile for various use cases.

Another minor benefit to using the DATEDIF function when calculating the difference in days between two dates over the simple subtraction method is if the input dates contain time.

11 June 202011 December 2020=B1-A1193
21 June 202011 December 2020=DATEDIF(A2, B2, "D")193
31 June 2020 8:00:0011 December 2020 10:00:00=B3-A3193.083333
41 June 2020 8:00:0011 December 2020 10:00:00=DATEDIF(A4, B4, "D")193

You can still achieve the same answer as the DATEDIF function, but you would need to remember and apply to use the TRUNC formula to achieve the same answer in row 3 above, like so:

51 June 2020 8:00:0011 December 2020 10:00:00=TRUNC(B5-A5)193

DATEDIF Errors and Problems

There are a couple of things to be mindful of when using the DATEDIF function. As already shown above in the [DATEDIF days examples]({{< relref “#date-difference-in-days” >}}) we had a result on the last row that gave us an error.

If you do get a #NUM! error check the parameter values are correct, and the earliest date is the start_date value, and the later date is the end_date value.

Provided your parameter values have been set correctly, the only other type of error which may cause problems and may not be as obvious is if the locale setting of your dates are not what you anticipated.

Incorrect DATEDIF Locale

If you’re getting an answer which definitely isn’t correct you will want to check the settings of your spreadsheet to ensure it correctly interprets a date field value.

For example, some countries, such as the UK and Australia, have dates set to the following format: DD/MM/YYYY whereas other countries, such as Canada and the Unites States, have date values set to the format of: MM/DD/YYYY.

101/06/202011/12/2020=DATEDIF(A1, B1, "D")193 for DD/MM/YYYY folks, or 311 for MM/DD/YYYY folks!

Ways to minimise this error occurring would be to quickly check your locale settings are correct before starting a new spreadsheet, or when modifying a spreadsheet.

In a Google Sheet this would be simply going to File > Spreadsheet Settings > Locale – change this to your country.

{{< figure src=”/images/datedif-check-locale.png” caption=”Check spreadsheet locale settings” alt=”Google Sheets locale setting” >}}

Another way to check is to input a date that wouldn’t be a date in the other format, for example, try 01/13/2020 and 13/01/2020 – one of these will not work and your DATEDIF formula will return a #VALUE! error as the date fields entered aren’t actual dates.


In this article we’ve done a deep dive into the DATEDIF formula and seen the results it provided based on different parameter values.

We’ve also seen how the formula can be better suited than simply subtracting two dates, and we’ve diagnosed our problems and fixed them.

Overall the DATEDIF function is a fantastic function to use, and you should look at incorporating it more in your own daily use. If you’d like to learn more about the function in Google Sheets, you can read the [documentation]({{% relref “datedif-docs” %}}).

Cloud Apps

3 Simple Steps To Send Email With Amazon SES

If you have a custom domain and want to be able to send email through your personal Gmail account without having to register for a Business Gmail account then here’s how you can do it using Amazon’s SES.


If you haven’t done so already you will need to set up your custom domain in Amazon’s SES. Here is a detailed step-by-step guide on how to create your email accounts using Amazon SES, Lambda and S3.

Once you’ve completed the steps on setting up your custom email address you can proceed with setting up the ability to send email from Gmail.


The steps in this guide to send email from your custom domain within your personal Gmail account will be as follows:

  1. Create an SMTP account with Amazon’s SES.
  2. Register your SMTP credentials in Gmail.

Let’s go through each of these steps in detail.

Step 1 – Create SMTP Account in Amazon SES

You will need to login to your AWS Amazon account and then from the Services menu click on Simple Email Service.

From the SES page on the left sidebar click on the label SMTP Settings.

Here you will see some core configuration settings for your SMTP credentials which you will need so you might want to copy them to your text editor:

  • Server Name:
  • Port: 25, 465 or 587
  • Use Transport Layer Security (TLS): Yes
  • Authentication: Your SMTP credentials.

Your configuration for the Server Name may be slightly different if you have selected a different location (look up at the top right-hand corner next to Support – to see which region your domain has been registered).

Register SMTP Credentials

To finish off this process click on the Create My SMTP Credentials button and follow through each of the prompts.

Create an IAM User Name to distinguish this user’s credentials. Once you have a basic user name then click on Create.

This next screen is very important as it will contain the unique user name and password that Gmail will need to create your SMTP account. Your username will be a combination of capitalised letters and numbers, and the password will be somewhat the same but with lower case letters.

Once you have captured these details and stored them safely it’s time to head on over to Gmail to register our new sending email address.

Step 2 – Register SMTP E-Mail in Gmail

Log in to your personal Gmail account, eg., and once you’re in click on the settings icon (usually an icon in the form of a bolt) followed by the Settings drop down menu item.

From the Settings area click on the tab with the label Accounts and Import, then find the area labelled Send mail as and within this area click on the link Add another email address.

This should pop up a modal window where you need to enter the following details:

  • Name: Your Name
  • Email address:
  • Untick “Treat as alias” as you capture email through Lambda & S3 if you’ve followed the [prerequisite]({{< relref “#prerequisite” >}}) above.

On the last window enter the following details you’ve captured from Amazon SES:

  • SMTP Server: (the Server Name)
  • Port: 587
  • Username: SMTP credentials username
  • Password: SMTP credentials password
  • Select Secured connection using TLS (recommended)

Finally, click on the Add Account button and then Google will send through an email you will need to open and click on a link to verify.

If you’ve set up your Lambda and S3 inbound email service to forward through any emails to this very same Gmail service you’ve logged in as you will get an email very shortly. Otherwise, go and fetch the email to where you’ve set the custom domain to forward emails through to and click on the link.

Once you’ve confirmed the link you’re ready to send emails from within your personal Gmail service! All you need to do to send an email using your custom domain is in the From field of your email, to click on the tiny down arrow alongside your email address and then click the domain you wish to send your email from.

Easy as.


In this article we have learned how to set up an Amazon SES SMTP service by creating a unique username and password. You’ve been able to use the configuration settings on Amazon SES to then configure sending emails from your custom domain by using these settings in their respective fields in Gmail’s Settings area.

If you use software to manage your email, you can copy the same process over and apply the same settings.

Cloud Apps

How To Create A Secure S3 Website Using Cloudfront & AWS Certificate Manager

If you have made the migration to host your website using Amazon’s S3 service and encounter issues with serving your content through a secure certificate or even trying to get Cloudflare to connect then this article will hopefully help set your connection right.

To help illustrate the concepts behind the connection, I’ll use a site I once owned to show the entire process

Step 0 – Log in AWS & Cloudflare Accounts

This is perhaps the easiest out of all the steps, although the rest aren’t too bad, but to get started you will need to log in to your AWS and your Cloudflare accounts.

Our first X steps will be in the AWS area, then towards the end we finish up with Cloudflare.

So, starting in your AWS account let’s get the following configurations set up.

Step A – Create an S3 Bucket (or two)

If you haven’t done so already you will need to create an S3 bucket to be able to host your static files.

Click on the orange Create bucket button and enter the web address of your website. For example, if your web address is then you would enter this into the Bucket Name field. If you want to host your website from a specific region then select this region in the next field. Lastly, as our static site is to be viewable by the public we need to untick the Block all public access checkbox and confirm this selection by acknowledging the warning that pops up underneath.

For my old website this creation of the S3 bucket stage would look something like this:

Create S3 Static Website
Create bucket for static hosting in Amazon S3

You’ll know everything has worked when you see a green confirmation pane, and a listing of all your S3 buckets.

Configure Your S3 Bucket for Static Hosting

There are a couple of further configurations still needed on our bucket to enable it for static site hosting. Therefore, click on your S3 bucket in the table before you and from this new page click on the Properties tab.

On the properties screen we have a couple of options to further enable static site hosting, the most obvious being the tile labelled Static website hosting – click anywhere on the white space in this tile (i.e. everything but the Learn More link):

Set Static Hosting Properties S3
Set static website hosting details for S3 bucket

The available options on this tile should be fairly obvious, we want to select the option to Use this bucket to host a website and upon selecting this option we need to set what the index document is when people enter our web address in their web browser, and what the error document is when people type an incorrect web page for our website.

As I use Hugo to generate my static sites, it automatically creates a 404.html page as the error page, therefore my settings for my S3 bucket would be as follows:

Set S3 Static
Set S3 static website index & error pages

Before we leave this window you’ll need to grab the URL. The URL generated by AWS for my S3 bucket is Keep a copy of this URL as it will be needed later.

When you’ve copied the URL click the Save button.

Create Bucket Policy

Once you’ve set the properties of the bucket, you need to set the Bucket’s policies.

Click on the Permissions tab, then click on the Bucket Policy button.

Below the main text box is a link labelled Policy Generator, click this, and you will be navigated to a visual editor where you can set your permissions.

On this screen set the following values:

  • Step 1: Select Policy Type = S3 Bucket Policy
  • Step 2: Effect = Allow
  • Step 2: Principal = *
  • Step 2: AWS Service = Scroll down and tick GetObject
  • Step 2: Amazon Resource Name = Type in your bucket according to the format arn:aws:s3:::/ (mine would be
  • Step 2: Add any conditions if needed (I didn’t set any).

Here’s how my options appeared:

S3 Bucket Statement
Add S3 bucket statement to bucket policy

Click the Add Statement button, and your settings would be added to the policy:

S3 Statement
S3 statement added to bucket policy

To generate the JSON code to use in our Bucket Policy you need to click the Generate Policy button found in Step 3.

Doing so pops up a modal window where you need to copy the contents of that window and then paste it over to the text box area above the Policy Generator link.

The code generated for my S3 bucket was as follows:

  "Id": "Policy1590953744901",
  "Version": "2012-10-17",
  "Statement": [
      "Sid": "Stmt1590953064638",
      "Action": [
      "Effect": "Allow",
      "Resource": "*",
      "Principal": "*"

When I copied this back into the bucket’s Bucket Policy text area and then clicked Save you should see confirmation of the nature of your bucket’s policy being public.

S3 bucket Policy JSON
Set S3 bucket policy JSON

Your bucket has now been set up to correctly display web pages. You can upload a file to your bucket and test this works by navigating to the URL you copied earlier.

Test S3 Bucket

To test your S3 bucket will correctly display web pages, create a simple index.html (or whatever identifier you used when setting the website’s index page in the Properties window).

I created a simple web page as follows:

<!DOCTYPE html>
<html><body><p>Hello world!</p></body></html>

Then upon clicking on the Overview tab in the bucket, click on the Upload button.

  1. Add your files from your computer. Click Next.
  2. Set permissions (I left everything as is). Click Next.
  3. Set the properties of the file (I left it set to Standard). Click Next.
  4. On the Review screen click Upload.

To test your page works go to the URL you copied in your web browser.

Test S3 Bucket
Test S3 bucket works by navigating to your bucket’s URL

If you can see the text of the HTML document you uploaded into your bucket then well done! If not, click on the Properties tab in your bucket, then click on the Static Website Hosting tile and click on the link to your bucket.

If you still cannot see anything check the settings on this tile for the Index document, and the name of the file you uploaded – they need to be exactly the same.

Create Another Second S3 Bucket

If you’d like to create another S3 bucket which automatically routes traffic through to your previous S3 bucket you’ve just created then do so now.

Go back through the previous steps, except this time name the bucket differently according to the web address you wish to redirect.

For example, I didn’t use the www sub-domain for my website address, but I wanted to redirect users who type the popular sub-domain in their address bar so that it redirects them correctly to Therefore, I created a new bucket labelled

The only difference with this set up is we only need to set the Properties on this bucket.

Therefore, once the bucket has been created, click on the Properties tab, then click on the Static website hosting tile again.

You’ll need to set the properties here a little different from your main bucket. Click the Redirect radio button and enter the name of your main bucket and whether to redirect traffic to a secure website or standard.

Here’s how the properties of my redirected bucket looked:

Redirect S3 Bucket
Set S3 bucket redirect properties

Click Save, and your redirected website is good to go!

Step B – Create SSL Certificate

If you want your bucket to be served through a secure connection (highly recommended) then you’ll need to use another service in AWS called AWS Certificate Manager.

By clicking on the Services link in the upper-left, type Certificate Manager and select the option that appears.

To start this process click the Get Started button to Provision certificates.

I found I had to set the zone for my certificates to N. Virginia to get them to work properly with Cloudflare. If you experience any problems with connecting your certificates to Cloudflare you might want to do the same.

To continue through you will want to select Request a public certificate, upon clicking the Request a certificate button you will be presented with a list of steps, let’s go through these one by one.

Step 1 – Add domain names

Add the domain names you want this certificate to cover.

I wanted to be specific, so in this area I entered each of the following domains:
Domain Names
Add your domain names one at a time

Click Next.

Step 2 – Select a validation method

With this step you need to prove you own the domain by performing one of two tasks: modifying your DNS records, or through email verification.

If you’re comfortable and have access to your domain’s DNS records then select the DNS Validation option.

Step 3 – Add Tags

If you’d like to manage your certificates by applying tags you can do so here, otherwise click Review button.

Step 4 – Review

The final step is to review everything you’ve done. If you’re happy with everything then click the Confirm and request button.

Step 5 – Validation

On this final step, if you’ve previously selected DNS validation you will be required to create a unique CNAME record for your domain.

AWS Certificate Manager DNS
Validate by DNS in AWS Certificate Manager

Here I need to set the CNAME record to verify both the root domain and the www sub-domain.

Once you’ve set these properties in your DNS provider you need to wait for AWS to validate and confirm your domain.

AWS Validated Certificate
AWS Validated Certificate for your domains

Step C – Create Cloudfront Distribution

Our final step is connecting both the S3 bucket with the secure certificates generated.

Click on the Services link again in the top-left hand corner and enter Cloudfront.

Click on the Create Distribution button to get started.

Step 1 – Select Delivery Method

Click on the Get Started button for Web.

Step 2 – Create distribution

On this screen you need to set a few properties, as follows:

  • Origin Domain Name = Paste the URL of your bucket’s website address without the leading http value. For example, the URL of my S3 bucket is but I just pasted into this input field.
  • Viewer Protocol Policy = Set to Redirect HTTP to HTTPS
  • Compress Objects Automatically = Yes
  • Alternate Domain Names (CNAMEs) = I entered per line each of the domains I had registered with the Certificate Manager, namely:
  • SSL Certificate = Set Custom SSL Certificate ( and then select the domain of the Certificate you created previously.
  • Default Root Object = Set index.html or whatever you set in your websites properties when setting the Static website Hosting.

Additionally, change any of the other settings as needed. Once you are done, click on the Create Distribution button.

You’ll need to wait a few minutes while the distribution creates the necessary configurations, and you’ll know everything is ready when you see the state of the distribution as Enabled

Cloudfront Distribution
Cloudfront distribution has been enabled

Step D – Set Domain CNAME Record

Finally, to finish this whole process off all you need do is set the unique Cloudfront URL to the value of your domains in your DNS provider.

My Cloudfront URL is, therefore, the property value for the CNAME records of my root domain and www sub-domain would be this Cloudfront URL.

To check everything has connected properly, and the domains have propagated through you can test this by running a dig command in your terminal:

$ dig

When you see some A records with IP address numbers then you’ve likely got things connected.

The ultimate test is to type your domain web address into your web browser and see the page you uploaded to your S3 bucket back in [Step A]({{< relref “#test-s3-bucket” >}}).


In this article we have learned how to create a secure website using Amazon S3, AWS Certificate Manager and Cloudfront.

You should be able to click on the lock icon in your web browser to inspect the secure certificate served.

Here was the final result of my website:

Secure Connection
Testing your connection is secure, click on the lock icon in your web browser
Certificate Valid
Checking the certificate’s validity

Cloud Apps

How to Create a Google App Script Library

In our previous post on clasp and Google App Script code we discovered how we can pull code down locally, do our edits and then push it back to Google Apps.

This is great when we’re working on code on the back-end of Google Sheets, or Google Forms, or any of the other great Google Apps.

However, what do you do when you want to write code, but you don’t want someone ‘accidentally’ editing your code?

Is there a way where you can write your code and then link to it within a project and have it run without the worry someone will break it?

Yes, of course!

We will still be using the fantastic processes of clasp, but this time we’ll be creating a new project. So instead of cloning a project to our local computer we’ll start by creating a new directory and creating a project there.

Create project

Create a new project with WebStorm of your favourite code editor. Initialise npm in your new directory (if you haven’t already done so):

$ npm init

Install clasp globally if you haven’t already done so:

$ npm install @google/clasp -g

Or check you have the latest version:

$ clasp -v

To create a new project it’s just a matter of making sure you’re logged in and progressing through the prompts:

$ clasp login

Then simply creating a new project:

clasp create [scriptTitle]

Create Ignore File

You’ll want to exclude files from being a part of the project, so you may want to look at creating a .claspignore file. My file tends to have these added:


Add gas-lib

Before jumping into the code you may also want to install a developer dependency to your project if you’re interfacing with Google Apps. I’d highly recommend adding a library to help with code completion:

$ npm install --save-dev gas-lib

Write Your Code

The next part of the journey is all over to you as you write your code.

If this takes time, and you want to keep your code safely stored on Google’s cloud then feel free to run the following command while you work away:

$ clasp push

Create version

Then when you’re ready to publish your code for publication and use you need to create a version:

$ clasp deploy

You should see a version number followed by a hash which represents the Deployment ID your script.

Do I need to re-deploy if I make changes?

Should you need to make any additional changes after having deployed your code you can still clasp push the code to Google Apps, but instead of using clasp deploy you will use the following command:

$ clasp version

The difference with this procedure is that you will need to provide a description for the new deployment version. As I find I’m only maintaining one source code most of my descriptions have been annotated with “fix X”, but feel free to use a better descriptor to suit your needs.

For example, if I’ve just made a fix to the code base on my computer, I could annotate the description like so:

{{< figure src=”/images/clasp-prompt-provide-description.png” caption=”Provide description to code changes” >}}


So now you’ve pushed your code up to Google Apps, how can you use it in the backend of other Google Apps?

Get Your Script ID

All you need is your Script ID for other Google App Scripts to use it.

Thankfully it’s just a simple command away. In your terminal enter the following command:

$ clasp open

When you do your script will open up in a browser window and on the terminal prompt you will see a URL which would have the following schematic:

Opening script:<SCRIPT_ID_HASH>/edit 

You can either try to capture the SCRIPT_ID_HASH from the URL in your terminal, or browser window, or you can fetch your Script ID from within the browser window clicking on File > Project Properties, and you should see on the pop-up modal a field labelled Script ID.

Import Your Code

Finally, to be able to import your code into another project you from the other projects’ code editor click on the Resources menu then click on Libraries....

You should see an area from this modal window where you can paste your Script ID:

Insert Script ID from Google App Script
Insert your Google App Script ID into the ‘Add a Library’ field

When you do successfully add your code you have the option of giving the library a callable name. I prefer to keep this as the default name of the library as it helps to keep things easy to maintain. If you do change the name, you may find having to update the name each time you update your version!

Here I’m adding Google App’s OAUTH2 library to my code, which is the same process for adding your own:

Add Library to Google App Script Project
Once you’ve inserted the correct Script ID you should see the library pop up above

Once you’ve then correctly added this Script to your project you must select a version to apply. Here I’m just selecting the latest version. Should the Script author update their code you will need to manually open this Libraries’ modal window and select the new version.

Select Latest Library in Google App Script Project
Best to select the latest version which will be at the top of the list

To reiterate again there shouldn’t be a need to change the Identifer field on this window, but if you must then do so here.

The purpose of the Identifier is that this is the referring Object name that is used in the projects code to use the functions of the inserted library.

Once you’re happy with everything just click the Save button.

Use It In Your Code

From here on out it’s now just a matter of referencing the new library in your code by using the Identifier.

So if we were using the OAuth2 library in our code it would look something like this:

var init = OAuth2.createService('QBO');


In this post you’ve been able to learn how to create a custom library in Google App Script.

You’ve seen how easy it is to deploy a new script, how to update it (if needed), how to import this library into your new projects, and how to reference it in your new project.

Creating your own libraries in Google Apps will help you to:

  • Reuse common libraries when interfacing with certain Google Apps
  • Prevent anyone ‘accidentally’ clicking around and removing your code
  • Easier access to code and to update, rather than trying to find the Document, clicking Script Editor (etc)

Have fun using Google clasp in your projects!

Cloud Apps

How to Flip ARRAY_CONSTRAIN Formula in Google Sheets

Previously I posted how you can use the INDEX() function to obtain the fields needed for a simple SUM() function.

Then I came across another handy Google Sheet function ARRAY_CONSTRAIN().


There are three parameters with this formula:

  1. range – insert the range for the formula to operate on.
  2. num_rows – set the number of rows to compress.
  3. num_cols – set the number of columns to compress.

All are required fields.


Let’s look at a current depreciation problem I’m trying to solve.

I’m trying to compress the different types of depreciation on an array of different purchases projected over the next few years and to calculate what the new depreciation would be once these items have been purchased.

The data looks like this:

{{< figure src=”/images/purchase-schedule-with-depreciation-life.png” alt=”depreciation schedule” caption=”Simple depreciation schedule” >}}

What we’re trying to achieve is to determine what the depreciation would be each year according to the year when items have been purchased, and their corresponding depreciation life in years.

Limitations of ARRAY_CONSTRAIN

Unfortunately the big limitation of ARRAY_CONSTRAIN is it works from the top-leftmost corner to cut the excess cells.

For example, if we apply the following formula on our data set:


It will return for us the following highlighted cells:

Array_Constrain Top Left Cells
ARRAY_CONSTRAIN() function only returns data from top-left cell

This causes a problem for our depreciation schedule as we’d like to fetch the last two years when we’re in the 2024 column for row 3, so using the following formula:


For the Network Upgrade row would only return [20000,<blank>], as these are the nearest to the top-left cells.


Is there a way to flip the input row?

There is!

Is there a REVERSE() function in Google Sheets we can apply to the range?

Unfortunately not yet.

So what we need to do as a workaround is to create our own REVERSE() function by sorting our data and reversing it backwards.

Thankfully my data already has a year row (Row 1), all I need to be able to do is sort the data set by that row, apply descending sort and then insert the new range back into the ARRAY_CONSTRAIN() formula.

Here’s how I flipped my data range:


Let’s break apart this function and understand what’s happening, as usual we’ll start with the innermost functions and work out way out:

  • ROWS($C$1:G3) – I used this to be able to get the column number to return from the QUERY function. As the last Column in the data set is the one I wanted, I just obtained this from the height of the original input range.
  • TRANSPOSE($C$1:G3) – as my data set is in rows, I need to translate this into columns for it to be functional as a data set with the QUERY() function. The data set needs to include the row containing the years.
  • QUERY(..., "SELECT Col" & ... & " ORDER BY Col1 DESC", 0) – the second SELECT parameter to our QUERY() function fetches just the one column, but orders by the first column being the column containing all the year values. The third parameter 0 just makes sure we don’t return any header rows.
  • ARRAY_CONSTRAIN(..., $B3, 1) – we finally wrap everything back into our original ARRAY_CONSTRAIN function. This time we change the parameters a little as they are in a column format, but the purposes are still the same – we want the nearest 2 years (the value of cell $B3) – and as there’s only 1 column our third parameter is 1.

To calculate depreciation on this row our final touch is to sum everything returned from the range and divide by the number of years:


We further wrap the formula in a check to see if there are any depreciation values in the Dep’n (Years) column:



Here’s the final result showing the function as well as our accumulated depreciation for the items purchased:

Calculate Depreciation Using Query & Array_Constrain
Calculating different depreciation schedules using QUERY and ARRAY_CONSTRAINT in Google Sheets


In this post you’ve been able to discover and learn about the ARRAY_CONSTRAINT. You’ve seen its limitations in fetching from the upper-leftmost corner, and perhaps in time Google Sheets will append an additional parameter to set where the corner should be fetched from.

In the meantime we’ve discovered how we can reverse or flip a range to make use of the ARRAY_CONSTRAINT function by using the TRANSPOSE and QUERY functions together.

Cloud Apps

Ignore Blank Cells in QUERY Function Google Sheets

If you have a series of data where you’d like to apply the QUERY function over, but want to ignore certain rows if a particular cell is blank or empty then you would write your formula as follows:


The way you can have the Google Sheets QUERY function ignore blank or empty cells is by using the string IS NOT NULL in your formula.


As an example, suppose we had the following data on assets we’d like to purchase that have differing depreciation rates, but we only want to return the items where there is a depreciation value (implying purchases with no depreciation will be written off in the year they are purchased):

QUERY Function Remove Empty Cells
Remove blank cells from QUERY

In the above diagram we have the following function:


Here’s what each of the parameters passed into the QUERY function mean:

First Parameter – Data Range

Set the data range for which the QUERY function will be performing its task.

In the example above the QUERY is performed on the data inputs for each of the large purchase rows.

Second Parameter – Query Filter

Applying a filter on the data range by using a form of SQL language. In our example, here’s what the statement translates into:

  • SELECT * – select everything from the range;
  • WHERE B IS NOT NULL – apply the filter on column B where the cell is not null (is not blank or empty).

Third Parameter – Header Rows

Although our data set does contain a header row (row A) we don’t want to return this in our result, therefore, we set this value to 0.


The result of our formula is values from the original data set which need to be depreciated over several years.

We can further apply more logic to our QUERY function to perhaps SUM the value of all assets purchased containing the same depreciation value.


In this post we’ve explored the powerful QUERY function and how we can filter our data by excluding rows where they contain cells which are blank or empty.

By applying the condition IS NOT NULL in our QUERY filter we can easily remove these from our results.

If you’d like to learn more about QUERY functions, especially how we can merge multiple rows into one you should check out our illustrated step-by-step article.

Cloud Apps

4 Simple Steps to Merge 2 Columns Into 1 With QUERY Function

If you have two or more columns of data and you want to merge these columns into one column then using the Google Sheets QUERY formula may be one way you can achieve this.

Here’s an in depth explanation of how to achieve this:

Step 1 – Prep your data

If you data doesn’t contain any spaces then you’re good to go, if though your data does contain spaces then you will need to define a character at the end of each cell to get this to work.

Step 2 – Understanding the QUERY Function

Let’s have two simple data sets, one which does not contain spaces in its data set, and another which does:

Simple data set with QUERY no header
Simple data set showing QUERY function with no header
Data set with spaces & QUERY with no header
Simple data set containing spaces showing QUERY function with no header

With both data sets we have highlighted in grey the returned values from the QUERY function.

QUERY parameters

The QUERY function is a powerful formula in Google Sheets helps to treat data contained in tabular format like a database table which we can perform basic SQL queries on.

This function contains three parameters:

  1. The data set.
  2. The SELECT command.
  3. The number of headers in your data set.

It’s the third parameter of the QUERY function that will help us in being able to concatenate the data within each column.

At the moment, from above, we have set the third parameter to 0 which means our data set does not return any headers.

But what if I want to define the first row as being the header?

Then we would simply change this third parameter in the QUERY function to 1 – representing the first row. When we perform this change to our formula we see no change to our data, however, the returned values have now defined that first row as a header area.

Define header row in QUERY function
Defining the first row as header

QUERY Header Parameter

What if we were to increase the number represented in our QUERY function? How is the result set returned if we define the header parameter as 2?

Here’s how it looks for our data sets:

Data set QUERY with 2 header rows
Query function with 2 header rows defined

Did you notice what the QUERY function does when we define more than one header row?

It concatenates the header rows and inserts a space between each appended element. Therefore, the first row containing A and the second row containing B produce the header A B.

(Can you foresee the issues we’re going to run into when we merge our columnar data which contains spaces already?)

Data set with spaces 2 header rows
Query function with 2 header rows defined

So if we wanted to get all the elements within our column wrapped into one row we would just need to change that third parameter to the maximum height of our columns. We could use the ROWS() function to capture this number:

Use QUERY Function to Merge Column Into 1 Row
Capture all rows into the header

Step 3 – JOIN columns

As the columns have been merged into one header row, we now want to stitch these column headers together.

This is achieved by wrapping the QUERY function in a JOIN function, like so:

JOIN QUERY data set header rows
JOIN query header rows together to combine into one cell

Notice here we used the first parameter of the JOIN function as a space to mimic the behaviour of what the QUERY data did to our data too.

Do be aware the JOIN function does have a limit of 50,000 characters. If you have a lot of data to concatenate you might want to break it up to smaller data sets.

What if our data contains spaces?

As you can see this strategy isn’t going to work too well as our result now contains too many spaces between data cells, and looks as follows:

JOIN data from QUERY for data containing spaces
The problem with this strategy is data containing spaces

Therefore, we’re going to need to improvise on the spaces in our current data set.

What we’re going to do is to modify our original data by substituting spaces for an oblique character that isn’t used in our data. For example, my current data set with names isn’t likely to have a tilde character ~.

Therefore, what we want to be able to do is to change our imported data set in the QUERY function so that we are operating on data that doesn’t contain any spaces – we want to be able to reserve these spaces for the explicit purpose of being able to split our data back into cells again.

One such way I achieved this was by changing our original formula:


To this:


What has happened with our first QUERY parameter?

Instead of referencing the original data set we modify the data set to substitute spaces for our oblique tilde ~ character.

The ARRAYFORMULA provides us the functionality of being able to operate on each cell within the range, and all we want to do is apply the SUBSTITUTE formula to each cell – if it has a space character replace it with the tilde character.

The result of our change should produce something like this:

Query ArrayFormula Substitute Spaces
Modify each cell in our data set by using ARRAYFORMULA and SUBSTITUTE

We’ve now achieved our purpose of reserving the space characters for another specific purpose which we’ll see next.

Step 4 – Transform back to one column

Our data is in one cell, split by the space character.

To stretch this back out into one single column all we need to do is SPLIT the data by the special space character and then TRANSPOSE it to leave the data as a column, otherwise it will remain in row fashion if we don’t.

Here’s what happens to our first data set when we SPLIT first:

=SPLIT(JOIN(" ", QUERY(A1:B4,,ROWS(A:A)))," ")
Split data after JOIN and QUERY
SPLIT data after JOIN & QUERY functions

Then our concluding formula for the first data set:

=transpose(split(join(" ", query(A1:B4,,rows(A:A)))," "))
Transpose data after split

What about our data set with spaces?

A little more work is going to be required with our original data set which contains spaces.

If we copy the above formulas into our working “space” formula here’s the result:

Transpose data after split
How our ‘spaced’ data looks applying the same TRANSPOSE and SPLIT formulas

As you can see we need to remove at the end each of those tilde characters.

How do you think we could do this?

If you were thinking ARRAYFORMULA and SUBSTITUTE again – well done! You’re acquiring a new skill today.

Here’s our concluding formula:

ArrayFormula Substitute, Split, Join & Query Functions
Final formula for concatenating multiple columns into one using QUERY function.


So there you have it!

Within this article you’ve been able to learn the following function skills when using Google Sheets:

  • What the third parameter of the QUERY formula does.
  • How we can modify each cell within a range by using ARRAYFORMULA – specifically using the SUBSTITUTE formula.
  • How to handle different data sets with the QUERY function, especially if they contain space characters.
  • How to JOIN our QUERY data to form a single cell of data (careful of the 50,000 character limit).
  • How to SPLIT our data set back into cells.
  • How to TRANSPOSE our data set back into columns.

Well done if you’ve been able to follow along and understand these complex topics.

Cloud Apps

Easily Merge Multiple Columns Into 1 with Query & ArrayFormula

In today’s post I’d like to explain how I used the following formula to help merge multiple columned data into a different columned arrangement as the purpose of the result was to export the original data set into a CSV file for import into NetSuite.

Here’s the formula we will dissect:


The problem

I had exported the following salary data from our budgeting software which contained how much each staff person would be receiving each month throughout the financial year.

An example of the raw data looked something like this:

123 Smith, Bob2,0002,200
456 Doe, Jane3,0003,300
789 Doe, John 200

The original spreadsheet had all months of the year and is labelled as Data:

Original Data set Employee Salary per Month
Employee salaries per month

I had to import this data into NetSuite using the CSV Import process, which required changing the structure a little bit, so that the resulting CSV looked like so:

Employee IDEmployee NameFrom DateTo DateAmount
123Smith, Bob1/01/202031/01/20202,000
123Smith, Bob1/02/202029/02/20202,200
456Doe, Jane1/01/202031/01/20203,000
456Doe, Jane1/02/202029/02/20203,300
789Doe, John1/02/202029/02/2020200

As you can see with the requirements of the task, we need to be able to expand the data set to rows (as it’s currently laid out in columns), then merge the data back into one columnar set. Once we have achieved this we then use the handy File > Download > Comma-separated values (current sheet) option in Google Sheets.

Solution: Step 1 – Expansion

The first rule when manipulating data is to leave the original data set intact.

Therefore, we create a fresh new sheet and label this whatever name is appropriate for your needs. In this example we will name it appropriate to what we are doing: DataExpand.

In our expansion we want to achieve all necessary CSV fields required. This means we need to fetch the ID number from the original Employee field, and to copy the remaining data into another field for the Employee Name. Then we split the header column in our original sheet to a data range. Followed lastly with the amount in each date range.

Our first sheet, should look a little something like this:

Expand Employee Salary data
Perform the CSV requirement per month

Here are the details as displayed in the photo above:

  • Cells $B$1 and $G$1 contained the =COLUMN() reference to each of the months on the original data set. This just helps with creating an easy index function that uses that reference which can make it easier copying the same columnar set 12 times.
  • Row 1 this contains the following formula to extract the ID number from the Employee field. Here we check to make sure we have data in the original Data sheet, if we do we want to perform a regular expression where we just want all the digits, therefore, we use the regular expression of d+:
  • From $B$2 down we then use another regex formula to extract those digits and replace them with nothing. We also want to check if the employee received any salaries or wages, if they didn’t we don’t want to add them into the data set.
  • From $C$2 down we just insert the ID that is needed for each row if we have a confirmed value in the previous column.
  • For $D$2 down if we have a value in $B$2 then we get the value of the salaries for that respective month. We use the handy index function here to get that value:
  • For $E$2 down if we have a value in $B$2 then we get the column heading of the respective month using, once again, the index function:
  • Lastly, for $F$2 down we get the value in the previous cell, if something exists, and calculate what the last day of the month will be for that cell:

As we have been mindful of the formulas used for each budget month, we now just need to copy cells $B$2:$F$1 and then paste this 12 times across. We could make it even easier by, after our first paste in cells $G$2:$K$4 changing cell $G$1 to be a formula: =$B$1 + 1:

Employee Salaries change header to formula
Change header row on first paste to formula

Then we copy this newly pasted range with formula edits across 10 more times.

Solution: Step 2 – Concatenation

The purpose of the next sheet is to zip up our columnar collections. This is a relatively simple step, however, there a couple of things to be wary of:

  1. Use TEXTJOIN when zipping up the columnar data rather than the JOIN or CONCATENATE functions. The primary reason for this is where there is no data in a cell TEXTJOIN creates the blank join, whereas the other functions neglect it. As an example, if we had the following table:
2Missing Piece

When using =JOIN(",",A1:C1) it produces the result Missing,Puzzle,Piece for the first row and =JOIN(",",A2:C2) will produce Missing,Piece for the second row.

Whereas using =TEXTJOIN(",",FALSE,A1:C1) produces the result Missing,Puzzle,Piece for the first row and =TEXTJOIN(",", FALSE, A2:C2) will produce Missing,,Piece for the second row.

As the TEXTJOIN function allows for the flexibility of the concatenation of your columnar data it’s import the data remain as it is represented elsewhere if there happens to be blank cells in your data.

  1. The other important aspect with this formula is that we want to replace the space characters within our cells as the space character will be used to join and split data back into columns again later. Therefore, it’s important at this step (if your data also contains spaces) to replace the space character with an oblique character that is not used anywhere in your data set.

I am going to replace my space characters within my data to the tilde character ~.

Therefore, I have created a new sheet, which I’ve labelled DataConcat and cell $A$1 has the formula:

=if(len(DataExpand!B2),SUBSTITUTE(TEXTJOIN(";",FALSE,DataExpand!B2:F2)," ","~"),"")

Be mindful when transferring the data across to adjacent columns that you don’t copy paste or drag across the formula, as each column here needs to reference the collection in DataExpand. For example, in the next adjacent cell $B$1 the formula is:

=if(len(DataExpand!G2),substitute(textjoin(";",FALSE,DataExpand!G2:K2)," ","~"),"")

You will need to do this 12 times for each month.

Substitute TEXTJOIN on Columnar data
Combine your data ready for merge using TEXTJOIN and SUBSTITUTE (if needed)

Solution: Step 3 – Merge

So here’s where we merge all the columns from the DataConcat sheet in Step 2 above into one big column collection.


Wow, what a massive formula!

As with mathematics, we have to start with the inner most functions and work our way out. So let’s break up each component within this formula and explain what’s happening:

QUERY function


Here we use the QUERY function to capture the data we want to operate on (the first parameter), being all the columns in the DataConcat sheet, and we then want to combine these cells into one row. We achieve this by setting the number of headers (the third parameter) to the maximum number of rows in the DataConcat sheet. This means every row in the DataConcat sheet will be zipped up into one row!

The way the QUERY function zips everything up is by applying the space character to each new cell. This was why it was important for us to remove the space characters from our own data set before we applied this function.

If you’d like to know more about the QUERY function I wrote up a more detailed post here explaining this type of use.



The combination of these three functions is to merge the single row returned by the QUERY function into one column.

First we JOIN the rows together using the special space character which helps to delimit each individual row. Then we SPLIT by the space character which produces the rows across individual cells horizontally. Finally, the TRANSPOSE function translates the horizontal cells into vertical formation – a single column.

Unfortunately the JOIN function does have a character length limitation of 50,000 characters. You may need to break up your requirements or look at other means if this ends up being an impediment.


The last step is expand our singular column so that it can be represented as needed for CSV export. If we’re expanding data sets we need to use the ARRAYFORMULA function, as demonstrated here:

ARRAYFORMULA(SPLIT(SUBSTITUTE(transpose(...), "~", " "), ";", TRUE, FALSE))

The ARRAYFORMULA provides us with the ability to loop through each of the cells in our data set and to apply individual functions to them. In our case we want to:

  1. SUBSTITUTE back the oblique tilde ~ character with its original space character.
  2. SPLIT back the data we concatenated in the DataConcat sheet. However, we need to be mindful there may be instances where we have blank cells, and we want the SPLIT function to return the blank cells, therefore, we need to change our fourth parameter in the SPLIT function to FALSE (default is TRUE). By switching this to FALSE it turns consecutive delimiters like ;; into three separate cells, rather than just one cell.

With these functions all combined they produce the wonderful result intended, like so:

ArrayFormula Split Substitute Expansion
Expansion of our singular column using ARRAYFORMULA, SUBSTITUTE and SPLIT


Within this post we were able to learn how we can extrapolate data from one form to another using several intermediary steps and complex Google Sheet functions.

Hopefully this post has been helpful with learning:

  • How to organise your steps by leaving the original data set (use multiple sheets if needed).
  • How to use the QUERY function and especially its third headers parameter.
  • Why you would use the TEXTJOIN function over JOIN.
  • The limitations of the JOIN function.
  • How to expand your data set using ARRAYFORMULA with any function when needing to loop through individual cells in the result.
  • How to set the SPLIT function’s fourth parameter so that consecutive empty cells aren’t treated as one.
Cloud Apps

How to Easily Sum Columns with Dynamic Range

I had a case in a spreadsheet where I needed to test the cash flow of payments made against future loans.

However, when it came to scenario testing the cash flow we had the ask and answer the question on the viability of the organisations cash flow depending on the loan repayment schedule.

Would we be able to repay our loans back quicker? What would our cash flow look like if repayments were 10 years, or 15 years, or 20 years’ terms?

To enable the spreadsheet to calculate this on the fly, I had to find a way whereby I could set a field value to represent the number of years the loan was to be repaid and for the single row in the report reflecting the repayment schedule to show those repayments.

For those familiar with a cash flow statement you will know there’s generally a section within the Financial Activities to detail any financial repayments made, ours was labelled Principal Repayments as shown in the basic table below of the first few columns (as we had to perform a 20-year projection there were more columns than this):

1Cash Flow ReportYear 1Year 2
2-8Assumptions area
9Operating Activities  
10Adjustments to Income Statement$B$10$C$10
11-19etc …
20Total Operating Activities=sum($B$10:$B$19)=sum($C$10:$C$19)
22Investing Activities  
23CapEx Purchases$B$23$C$23
24-31etc …
32Total Investing Activities=sum($B$23:$B$31)=sum($C$23:$C$31)
34Financing Activities  
35New Loans$B$35$C$35
36Capital Grant Receipts$B$36$C$36
37Principal Repayments$B$37$C$37
38Total Financing Activities=sum($B$35:$B$37)=sum($C$35:$C$37)
40Total Net Cash Flow=sum($B$20, $B$33, $B$38)=sum($C$20, $C$33, $C$38)

Now the issue at hand was that the New Loans row was an input row. Management wanted to insert how much they were willing to borrow for each project, but they wanted to see the impact on the Total Net Cash Flow which was the sum of the Total Operating Activities plus the Total Investing Activities plus the Total Financing Activities.

So with an input field on the New Loans row, and the output field on another row Principal Repayments all I needed to do was one more input cell representing the Loan Terms (in Years).

The cell I used as the input for the loan terms is defined in cell $B$50.

So here’s how the Principal Repayments row ended up:

$B$37 = -sum(index($B$35:B$35,0,MAX(COLUMN(B$35)-COLUMN($B$35)-$B$50+1,1)):B$35)/$B$50

So let’s break this formula down, starting in the heart:

  1. MAX(COLUMN(B$35)-COLUMN($B$35)-$B$50+1, 1) this formula helps to determine the column to start our range capture.

Here’s what we would get if the formula was copied across different cells:

$E$35 = MAX(COLUMN(E$35)-COLUMN($B$35)-$B$50+1, 1)

Which for each reference and function call results in:

  • COLUMN(E$35) = 5
  • COLUMN($B$35) = 2
  • $B$35 = 10
  • MAX(5 - 2 - 10 + 1, 1) = MAX(-6, 1) = 1

For the cell $Z$35 which would contain this portion of the formula as MAX(COLUMN(Z$35)-COLUMN($B$35)-$B$50+1, 1) has the following results:

  • COLUMN(Z$35) = 26
  • COLUMN($B$35) = 2
  • $B$50 = 10
  • MAX(26 - 2 - 10 + 1, 1) = MAX(15, 1) = 15
  1. index($B$35:B$35, 0, ...) the nifty thing about the index formula is that it returns a value or the reference to a value. The parameters of the index function are: INDEX(reference, [row_offset], [column_offset]) and as we don’t want to change the row_offset this is represented as 0, but we do want to change the column_offset and this is where the MAX formula helped us. From the value achieved from the MAX formula we then receive a reference.
  2. sum(index(...):B$35) as we’ve captured the cell reference from the index function we can add the range notation : and have the sum value add all values from that cell to the current cell we are in.
  3. =-sum(...)/$B$50 lastly as we have added up all the new loans in the sum formula we then divide the total New Loans amount by the loan term.

So if we had this cash flow sheet spread out of 25 time periods out, by the time we got to the formula in $Z$37 it would look like this:

$Z$37 = -sum(index($B$35:Z$35,0,MAX(COLUMN(Z$35)-COLUMN($B$35)-$B$50+1,1)):Z$35)/$B$50

Thereby only getting us the last $B$50 (loan term) periods and dividing that amount by the loan term.

INDEX formula is a nifty little formula to learn!

Cloud Apps

How to Clone Google App Script Code to Your Local Computer

I’m starting a new project, and the requirements for this project is to write some Google App Script, copy the source code over to BitBucket. Thankfully there’s an easy way to store Google App Script locally to your computer and then push those changes back to Google as well as git commit changes and have the code stored safely on a remote repository.

Here’s how you too can set up your computer and environment up.

Install google/clasp

To install google/clasp jump onto your terminal, and if you haven’t (as I haven’t yet), initialise npm in your folder:

$ npm init
This utility will walk you through creating a package.json file.

After progressing through all the prompts I then run the command to install google/clasp by npm install clasp:

$ npm install clasp
Installing @google/clasp...
Installed @google/clasp@2.3.0

Your terminal prompt should inform you of the success of your google/clasp module installation.

Login to clasp

To have google/clasp know where to pull and push scripts to it needs to connect to your Gmail account. Therefore, the first command you need to run once you’ve successfully installed it is clasp login:

$ clasp login

A link then pops up underneath the prompt and when you click on it you will be redirected to a Google login screen where you will need to enter your credentials and provide the necessary permissions for the script to function properly. You will know everything is successful when you see back in your terminal window:

Authorization successful.

Clone script

If you have already started a Google App Script project you may be able to clone it to your current project. There are two ways you can try to pull in a project’s script:

  1. Try clasp clone and then select from the options presented by using your up and down arrow keys to navigate through the list.
$ clasp clone
? Clone which script? (Use arrow keys)
> ABC ...

If, however, you have many scripts, and the script you’re looking for isn’t presented you will need to exit from the prompt window using Ctrl-C.

  1. Enter the specific Script ID as a parameter to your clone command, like so:
$ clasp clone ABC123...

Where ABC123... is the Script ID you can obtain by opening up your Google App Script console for the project you’re hoping to clone and clicking on File > Project Properties. From the modal window that pops up, copy the Script ID hash.

Get Script ID for Google Clasp
Get your Script ID to easily clone a project to your computer using clasp

Ignore Files

If needed you might want to exclude certain files from clasp pushing them into your script folder in Google. If so, create a .claspignore file and populate it with files you don’t want to push up.

My .claspignore looks as follows:


Update code

To update your code on the Google Server you just need to push your code back up.

$ clasp push

You can check your code is working by opening your project’s Google App Script code.


In this short article you’ve learned the following:

  1. How to copy a project in Google Apps to your local computer using the clasp npm package.
  2. How to exclude certain files and folders from your project.
  3. How to make local changes and push your changes up to Google Apps.

If you have your code wrapped as a library which other code references you’ll need to create a version and then reference the new version in your code dependencies.

If you’d like to discover how you can create a library for your common Google Apps code, or to prevent someone from editing your code on your Google Sheets or other Google Apps, then check out my [next post on creating a Google Apps library]({{< relref “” >}}).