Example of pdf version from altc 2014At ALT we use Google Sheets as an easy way to share and collaborate on draft event timetables. Recent examples are the ALT Annual Conference 2014  and the OER15 timetables. One of the reasons for publishing draft timetables using Google Sheets is  we can get a static url for people to download it as PDFs but the contents can be dynamically updated (see recent post on doing this). The template we use for conferences is continually evolving which isn’t an issue as it’s easy to copy the last version. One headache is that our theme colour usually changes. This can be a bit fiddly change as we use empty cells to create a thicker grid:

Thicker borders

Faced with another cell background switch it made sense to actually do this with code rather than clicks and thanks to Google Apps Script possible in 19 lines of code and a couple of minutes:

function colorReplace() {
  var doc = SpreadsheetApp.getActiveSheet();
  // get all the existing active sheet background colours
  var cells = doc.getRange(1, 1, doc.getLastRow(), doc.getLastColumn()).getBackgrounds();
  var rows = cells.length;
  var cols = cells[0].length;
  // iterate accross
  for (var i = 0; i < rows; i++){
    for (var j = 0; j < cols; j++){
      if (cells[i][j] == '#feeff8'){ // first color to change
        cells[i][j] = '#f3f3f3'; // first color change to
      } else if (cells[i][j] == '#bf0875'){ // second color to change
        cells[i][j] = '#079948'; // second color to change
  // update backgound colours
  doc.getRange(1, 1, doc.getLastRow(), doc.getLastColumn()).setBackgrounds(cells);


To get the existing cell background colour I used the debugger setting a breakpoint before the loop to see the existing cell colour HEX codes:

debugger to inspect cell colours

When I fly it generally becomes a session in staring into space and letting my mind wonder. It’s not that I haven’t got a long list of Google Apps Script ideas I want to explore, but as a ‘cloud’ based scripting environment a data connection is required (insert pun about in the clouds/above the clouds). For my latest trip I decided to set myself a challenge – create a watch face for Android Wear only using my mobile. This is a lot easier than it sounds thanks to the WatchMaker Watch Face app for Android. Here’s how I got on (or you can just download my watch face)

My watch face on the right displaying the time 9:42 and on the left TokyoFlash Twelve 9-5 B it was inspired by
My watch face on the right displaying the time 9:42 and on the left TokyoFlash Twelve 5-9 B it was inspired by

WatchMaker is one of a number of apps that allows you to do WYSIWYG (what you see is what you get) editing. Making my own watch face was something I started thinking about at the weekend and I’d already scoped out WatchMaker, downloaded some other watch faces and had a peek under the hood. As part of this I discovered WatchMaker integrates with the Lau Programming Language making it possible to do some amazing stuff.

Some of the watch faces I've been playing withAnother great feature of WatchMaker is reuse is baked in. Every watch face you import can be duplicated and customised. This liberal approach makes it easy to see how other people are using WatchMaker features.

As part of my weekend tinkering I’d already taken a picture of my well loved TokyoFlash Twelve 5-9 B watch, shown above. The 1259 B uses LEDs to light up a binary display of the time (see video here), hours shown on the left and minutes equalling the sum of the top (ones) and bottom (tens) rows.

I wasn’t planning on making a watch when I sat down on the plane, but just before the aircraft doors closed grabbed an image of a 1259 B with LEDs on. As I wanted to use a 1259 B LED in my watch the first challenge was cropping the image. I’ve limited image editing apps on my phone but discovered if I successively used the Snapseed cropping tool I could get down to a 16x16px image.

Screenshot_2015-05-19-07-54-15[1] Screenshot_2015-05-19-07-54-09[1]

Adding an image to WatchMaker lets you position the image and appearance using a number of build in WatchMaker variables. For example, {dmo} returns the current minute in the hour in ones. So to progress a minutes led one step from left to right I set the x-position using –90+{dmo}*18. Rotating the hour LED around the bezel was a little more tricky and during the flight my basic trig. maths failed me but the cos/sin rule quickly came back to me once the aircraft doors opened.

There a lot more you can do in WatchMaker with sensor and external data and even animation effects and whilst my first foray in this area is basic it’s mine and my maker journey continues ;)

At ALT we are a Google Apps for Education user and make extensive use of Google Drive for creating and publishing documents. Some of our documents, such as member lists or timetables, get regularly updated and updating links in our websites can be a chore. One of the nice features of documents in Google Drive is you have a couple of ways of publishing documents so anyone can view (no log in required). For Google Sheets the main options are getting a shareable link:

share with others  ... because sharing is good ;)

Or using the ‘publish to the web’ option:

publish to web new school

In the ‘old’ version of Google Sheets the ‘Publish to the web’ option included lots of file types, not just as a web page:

publish-to-web old skool

You can still get new Google Sheets to generate download links for other formats … it’s just a bit complicated. The complicated bit is working out which url tweaks you need. For example, with your magic goggles on you might start spotting a pattern in this url which I’ve line breaked to make easier to see:


Remembering all these switches isn’t easy so for our team I’ve shared a simple template with lets us drop in a shared Google Sheet link, choose options for layout and get a url. Here’s a copy in case you find useful:

PDF Link for Google Sheet Template
PDF Link for Google Sheet Template


My old colleague at Cetis, David Sherlock, posted a nice little  ‘Twitter Question/Revision Bot’. This uses a .csv file of questions and multiple choice answers which get randomly tweeted out using a Python script. David designed the project with a Raspberry Pi in mind but also highlights it can be easily run on any Unix like environment such as Mac OS X or Linux. As not everyone is going to have easy access to this here’s how you can do something similar with Google Sheets (if you don’t want to play copy and paste coding make a copy of this sheet).

1. Setting up a Google Sheets environment to handle it

  1. Start with a new Google Sheet and like David have six columns in each row with question, answer, three options (one of which the correct one) and an extra row to record if the question has been asked.
  2. In your spreadsheet open Tools > Script editor and when asked start a ‘Blank Project’
  3. In the new editor window select Resources > Libraries (this will first prompt you to give your project a name, I called mine TwitBot.
  4. In the ‘Find a Library’ box enter MarIlVOhstkJA6QjPgCWAHIq9hSqx7jwh and click Select
  5. You should now see ‘TwtrService’ listed as one of the libraries. In the ‘Version’ dropdown select the latest version (at time of writing 14), and click Save

TwtrService is a library I’ve written so interact with the Twitter API. You can read more about it here.

In the code window add the following code and click save:

function setup() {
  if (TwtrService.isUserConnectedToTwitter()){
   var result = Browser.msgBox("Twitter Authorisation",
                   "You appear to already be connected to Twitter.\\n\\nWould you like to run the setup again?",
    // Process the user's response.
    if (result == 'yes') {
      // User clicked "Yes".
  } else {

The above code uses the TwtrService to help you set up Twitter access if required

Your script window should look like this:

script editor

2. Create a Twitter App

If you’ve used my other TAGS templates you can reuse your details for this. To see if Twitter App details are required from the script window select Run > setup (if setup isn’t listed you need to first save your code). Running setup will start the first part of the authentication process. Click continue and review the authentication required and ‘Accept’ if you are happy.

Auth required

Going back to the spreadsheet you started there should now be a dialog window asking you to do something. If you haven’t setup a Twitter App before it should look like this:

Twitter app creation

Follow the instructions onscreen to create your app.

Important: The Twitter account you use to authorise access is the one that will send out the tweets

3. Write a Python Google Apps Script

Add the code below to your existing script project and save:

var doc = SpreadsheetApp.getActiveSpreadsheet();
var sheet = doc.getSheetByName('Sheet1');

function tweetQuestion(){
  var ran = Math.floor(Math.random() * (sheet.getLastRow()-1)) + 2;
  var row = sheet.getRange(ran, 1, 1, sheet.getLastColumn()).getValues()[0];
  if (row[5] !== ""){
    tweetQuestion(); // if already asked pulls another random row
  } else {
    var tweet =  "Q: " + row[0];
    var tweet2 =  row[2];
    var tweet3 =  row[3];
    var tweet4 =  row[4];
    TwtrService.post('statuses/update', {status: tweet});
    var options = [tweet2,tweet3,tweet4];
    TwtrService.post('statuses/update', {status: "A: " + options[0]);
    TwtrService.post('statuses/update', {status: "B: " + options[1]);
    TwtrService.post('statuses/update', {status: "C: " + options[2]);
    sheet.getRange(ran, 6).setValue(new Date());

// http://stackoverflow.com/a/25984542/1027723
function shuffle(a,b,c,d){//array,placeholder,placeholder,placeholder

4. Time the script to run every hour or so

In the script editor select Resources > Current project’s triggers and click ‘No triggers set up. Click here to add one now.’. Select to run tweetQuestion every hour (or your preference), and also click ‘notification’ so you can get an email if the script fails. Finally click ‘Save’

Timed triggers

What will happen now is the function even if you don’t  have the spreadsheet or script editor open or even your browser.

Important: when this script runs out of questions it will go into an infinite loop. You can go back into the trigger window to remove the function at any point. If you don’t you’ll end up using all of your script runtime quota. You homework is to figure a way to get the script to bail if there are no questions left.

My homework...

function tweetQuestion(){
  var asked_col = sheet.getRange(2, 6, sheet.getLastRow()-1).getValues();
  // get unasked q's
  var unasked = [];
  for(var i=0; i < asked_col.length; i++) {
    if(asked_col[i][0] == "") {
  // randomly pick one
  var ran = Math.floor(Math.random() * unasked.length);
  if (unasked[ran]){
    var row = sheet.getRange(unasked[ran], 1, 1, sheet.getLastColumn()).getValues()[0];
    var tweet =  "Q: " + row[0];
    var tweet2 =  row[2];
    var tweet3 =  row[3];
    var tweet4 =  row[4];
    TwtrService.post('statuses/update', {status: tweet});
    var options = [tweet2,tweet3,tweet4];
    TwtrService.post('statuses/update', {status: "A: " + options[0]});
    TwtrService.post('statuses/update', {status: "B: " + options[1]});
    TwtrService.post('statuses/update', {status: "C: " + options[2]});
    sheet.getRange(unasked[ran], 6).setValue(new Date());
  } else {
    // no questions left - do something else

Kin Lane has done some great work in highlighting the importance of APIs in education. If you are unfamiliar with APIs they are a way for separate programs to communicate and share information or perform actions.  With the growing usage of data in education I believe APIs are the only way to use data effectively and efficiently. Kin’s University of API white paper  is a great starting point to get more context.

Reading the white paper reminded me how important it is to get people to think beyond the webpage and consider the underlying data used to generate it.

Luke, view the source

Back in the day when I discovered the work of Tony Hirst this was a real threshold concept for me. Five years ago unpicking data powering the web felt a lot easier there was usually only basic authentication required, if any. Now you usually have to do some sort of authentication handshake. This additional step often immediately lands you in codeland. Even if you don't do code there are still opportunities to explore APIs. Any decent API service will usually have interactive documentation for developers or API consoles. In a recent talk, which you can see the fragments of here, I highlighted what data is behind a tweet. If you'd like to explore the Twitter API in a non-cody way here's how:

Interactively exploring the Twitter API

1. Go to Twitter API console https://dev.twitter.com/rest/tools/console. Make sure you logged in (should see your avatar top right)

2. In the Authentication drop down select OAuth 1 - this will prompt you to sign in with twitter


3. When bumped back to the pack select /status/show/{id}.json

image (3) 

4. After it prefills some details switch to the Template tab. In the id box enter a twitter id number e.g. in my tweet https://twitter.com/mhawksey/status/591156241969319936 you'd just enter 591156241969319936 and hit the orange send button

 image (2)

5. In the pane you should get a response. The main data bit starts:


6. To get details of other tweets click the Template tab and enter another id.

image (1)

7. If you are interested in other API calls you can make click the Service box and select another



Later today (2.30pmUTC) I’ll be presenting at #oer15 about Twitter in open education (tune in here). As I wanted to highlight the network effect of Twitter I wanted to engage not just the room, but leave ‘footprints’ as for others to follow. I know people like Alex Couros and Alan Levine have done cool stuff live tweeting from Keynote. I’ve dabbled with doing stuff with Microsoft PowerPoint but was never fully satisfied. Given Twitter now supports a number of embedded media formats I thought rather than trying to fit Twitter into another presentation tool, to turn my live tweets into my slides.

And so TAGSPresenter is born! Using a Google Sheet as an editor, Google Drive to host images and a bit of Google Apps Script to glue it together I’ve got my own Twitter based presentation tool. I don’t have time to write about how it was technically achieved but if you want to peak under the hood of the hack here are my ‘slides’ which are published here.

Tune in at 2.30 to see how it goes ;)

In a couple of weeks I’ll be talking about TAGS at OER15 (14:30-15:00, 15th April). Whilst parallel sessions aren’t going to be streamed I’ve got a couple of ideas for broadcasting my session. If I pull this off I’ll be co-tagging my presentation #oer15 #740.

My notes for structuring the session so far is:

  • Networks – shape, strength and characteristics
  • Networked education – making the connection
  • Footprints – seeing the connection
  • Twitter in Ed – activities Adoption Matrix (Mark Sample) and examples
    ”anyone to become an active participant in the conversation” Ross, 2012.
  • APIs – me speaky code
  • Twitter Search and the Twitter Search API
  • Anatomy of a tweet
  • TAGS/TAGSExplorer
  • TAGS in the wild
  • Context – SNA, ethics, vulnerability

You’ll see I’ve highlighted two items in that list and this is where you can help. If you have used TAGS to support your class/course I’d like to know:

  1. how does Twitter generally fit in to the course? Are you using directed activities or is there a more organically;
  2. how TAGS is used to support this? Post/ongoing SNA, situational awareness, …

As the session will mostly be me talking, preferably some video or audio would be great. If you’ve previously talked about Twitter/TAGS in education and a recording is available I’ll happily look at this and see if there is something I can use.



There was a question that came up in the Google Apps Script G+ community about moving a row of data to another sheet. The person was reusing some code posted by Victor Yee back in 2012 which hooks into the onEdit event in Google Sheets. The idea is a Google Form is used to collect data into a Google Sheet. Someone then looks at the data entered and decides if it should be actioned. If yes then the data is moved to an appropriate sheet within the spreadsheet. The route of the problem appeared to be not only has Google Apps Script changed a lot since then but so has Google Sheets and Forms. In particular it looks like new Sheets “Cannot cut from form data. Use copy instead.”:

Cannot cut from form. Use copy instead

To use ‘copy’ instead in Victor’s code you would replace moveTo with:

s.getRange(rowIndex, 1, 1, colNumber).copyTo(target);

and add the line afterwards of


which will delete the row just changed. I’m not sure why moveTo doesn’t work. Perhaps there is conflict between the onSubmit and onEdit events. Looking through Victor’s code I was surprised he didn’t use the onEdit fields available. For example:

e.sourceSpreadsheetA Spreadsheet object, representing the Google Sheets file to which the script is bound
e.rangeRangeA Range object, representing the cell or range of cells that were edited
e.value10Only available if the edited range is a single cell

.. so I've reworked into:

 * Moves row of data to another spreadsheet based on criteria in column 6 to sheet with same name as the value in column 4.

function onEdit(e) {
  // see Sheet event objects docs
  // https://developers.google.com/apps-script/guides/triggers/events#google_sheets_events
  var ss = e.source;
  var s = ss.getActiveSheet();
  var r = e.range;
  // to let you modify where the action and move columns are in the form responses sheet
  var actionCol = 6;
  var nameCol = 4;

  // Get the row and column of the active cell.
  var rowIndex = r.getRowIndex();
  var colIndex = r.getColumnIndex();
  // Get the number of columns in the active sheet.
  // -1 to drop our action/status column
  var colNumber = s.getLastColumn()-1;
  // if our action/status col is changed to ok do stuff
  if (e.value == "ok" && colIndex == actionCol) {
    // get our target sheet name - in this example we are using the priority column
    var targetSheet = s.getRange(rowIndex, nameCol).getValue();
    // if the sheet exists do more stuff
    if (ss.getSheetByName(targetSheet)) { 
      // set our target sheet and target range
      var targetSheet = ss.getSheetByName(targetSheet);
      var targetRange = targetSheet.getRange(targetSheet.getLastRow()+1, 1, 1, colNumber);
      // get our source range/row
      var sourceRange = s.getRange(rowIndex, 1, 1, colNumber);
      // new sheets says: 'Cannot cut from form data. Use copy instead.' 
      // ..but we can still delete the row after
      // or you might want to keep but note move e.g. r.setValue("moved");

which you can also get by making a copy of this sheet (Update: remember to open Tool > Script editor and then click Resource > Current project triggers to add the onEdit event to the function). See also Michael's comment about using var s = e.range.getSheet();

A question came in on the Google Apps Script Google+ Community which in part was asking about parsing .csv files into a Google Sheet. I saw the question come in over my phone and knowing it was very achievable directed the person to the Interacting With Your Docs List tutorial on the Google Developers site as a starting point. In particular this tutorial included a function to parse a .csv file using regular expressions. Shortly after +Andrew Roberts kindly pointed out  that Apps Script includes a parseCsv method … doh. +Marcos Gomes then pointed out that the tutorial uses the Docs List Service that was deprecated on December 11, 2014 … doh. Given the tutorial I referenced was written in May 2010 it’s not surprising given the updates in Apps Script that it’s now out of date and the tutorial itself should probably be carrying a deprecated notice. One of the really nice things about the Google Developers site is most, if not all, the documentation is released under Creative Commons Attribution 3.0 License. So for my penance below is a reworking of Tutorial: Interacting With Your Docs List CC-BY Google Inc. (Jan Kleinert)

...continue reading

Back in 2012 I shared some of the exploratory work I did issuing Mozilla Open Badges with using a combination of Google Apps Script and Google Sites. This solution had bit of a fudge which required the badge Assertion (the JSON data file behind each individual badge), to be proxied via a web host. It was great to see this post was followed up by David Mullet who was able to add some additional functionality as well as avoiding the badge proxying. Since then I've also revisited Open Badges, this time as an add-on to issue badges from self-hosted WordPress sites. As part of this I learned more about issuing badges and in particular the Mozilla Issuer API. The Issuer API is a JavaScript library which makes it easy for you to let people add badges they've achieved to their badge portfolio in the Mozilla Backpack. Late last year I decided to revisit Open Badges and Apps Script, this time integrating the Issuer API into a Script powered web app, removing the need send people to a Google Site. This wasn't as straight forward as I had anticipated and but in this post I'll cover what is possible.


Survey participant emailSo why issue badges directly from Google Apps Script? This was inspired by feedback from a survey we were running at ALT. This was distributed as a Google Form. To help improve the response rate we included a feature to receive an email confirming the respondent's participation in the survey. The confirmation, sent using Apps Script, included a suggested tweet and a digital badge for them to display highlighting participation. Because the badge wasn't in the Open Badge format respondent's couldn't add it to their Mozilla Backpack. I could have of course reused the earlier work I and David have done on issuing badges via Google Sites, but wanted to see if it was possible to remove that dependency and issue Open Badges from a single container bound script or by embedding an Apps Script in Google Sites.

What’s possible?

Looking through the source code of the Issuer API you can see it has two methods for launching a badge claim interface. The default, OpenBadges.issue (code here) is a full screen modal interface created by injecting a iframe. The fallback, OpenBadges.issue_no_modal (code here), adds the badge data to a web form which is automatically submitted to the Backpack to handle. At the time I was exploring this problem, the method for creating html based interfaces in Apps Script was restricted to a sanitised version of HTML which didn’t support the modal/iframe way of doing things. That all changed in December when Google announced an alternative way of serving HTML in Apps Script that skipped most on the sanitisation (see Speeding up HtmlService in Apps Script).

This all sounds promising, directly wrapping the Issuer API JavaScript library in a Apps Script powered web app. Trying this out I hit my first issue:

I'm using Google Apps Script Content Service to generate a badge assertion (1.0.0 spec), example here https://script.google.com/macros/s/AKfycbx5uvVcmmHweZwhIxzO0IAUrUtY_cW88Sz1B-MpquZxopvfyIY/exec?type=assertion&uid=54354354354-2

When trying to issue the badge using the Issuer API the issuer returns a 504 bad gateway example header response. Using Google Apps Script Content Service there are 302 redirects are in place for the badge assertion which end in 200 OK (this is how Google configure their service). Here is example 302 redirect headers and example eventual 200 OK.

Using Google Apps Script Content Service to create badge assertions appears to only affect assertions that include verify.url (a 0.5.0 spec assertion is issued without issue).

A 302 redirect 1.0.0 spec assertion was issued okay e.g. http://labs.hawksey.info/badges/1_0_spec_redirect.json which suggests something else in the headers is causing the 504 bad gateway

If you fill in this form with your Backpack email it sent a link to a Google Site where you should be able to claim a badge associated to your email https://docs.google.com/a/hawksey.info/forms/d/1IKfJQGu1spJyTpkPWYL8TlXcRFQxm8gv8ZMATXcVQWU/viewform

I'm still getting a 504

A lot to take in here. Basically Open Badges can use a JSON object for each badge also known as the Assertion.  The Assertion spec is at 1.0 which requires  a verification url for the badge. The verfication url is the same as the url where the Assertion lives. Here is an example badge generated using Apps Script:

Now if you hit the verify.url: "https://script.google.com/macros/s/AKfycbx5uvVcmmHweZwhIxzO0IAUrUtY_cW88Sz1B-MpquZxopvfyIY/exec?type=assertion&uid=54354354354-2" you get passed through a 302 redirect:

302 redirect

In terms of Badge Verification “eventual 200 OK” is permitted:

The use of the term "eventual 200 OK" is meant to mean that 3xx redirects are allowed, as long as the request eventually terminates on a resource that returns a 200 OK.

Using the Open Badges Validator throwing in the verify.url returns a valid Assertion

valid Assertion

So the Mozilla Backpack issue ticket remains open. Currently, as far as I can see, a version 1.0 spec Assertion generated in Apps Script 504s in the Issuer API. If you are desperate to issue Open Badges in Apps Script NOW!!! the fix ain’t pretty … the Open Badges 0.5 spec assertion (bloody nightmare to find this reference …). This is basically where I was in 2012, a badge assertion that doesn’t use a verify.url.

Lets however look at a couple of example workflows for issuing Open Badges using Google Apps Script.

Example: Using a Google Form to Issue Open Badges via a Google Site

Back to the original scenario of giving someone an open badge for completing a Google Form.  Here’s an example Form/Sheet you are free to copy/reuse. Things to note are:

  • an extra sheet called IssuedBadges; and
  • some code in Tools > Script editor (source extracted into a gist) which needs you to go to Resources > Current project’s triggers and add a onSubmit trigger .

The code is hooked into the form submission and records a new badge in the IssuedBadges sheet and generates an email to the recipient to collect their badge (note current issue with duplicate emails from Apps Script). You’ll also see from the code it includes a BASE_URL, this is the place we’ll send people to  collect their badge. A Google Site can happily have a container bound Apps Script, but in this example I want to highlight how you can have a standalone script embedded in a Google Site. So our next bit of code is a standalone Google Apps Script which handles the Open Badges Assertion creation and issuing. You can copy and then modify the standalone script here or browse as a gist).

Hopefully the source has enough in-line documentation to make sense of it. Basically the code looks for the type of data it’s returning. If it’s another machine it throws JSON and for humans shows the Mozilla Issuer API which will help the person collect their badge in the url.  To deploy this code we need to publish it as a web app. If you haven’t done this before from your copy of the script in the Script Editor select Publish > Deploy as web app which should give a similar dialog to the one below:

Deploy as web app

A couple of neat things here. For this to work you need to allow the app to ‘execute as me’. This lets the script access the Google Sheets your account can access. This means the script will selectively be able to read the Google Form responses you get. We also allow the script to run for anyone, even anonymously. This doesn’t mean that anyone can start reading all of your data, the script is programmed only to ready specific data and only return a specific interface designed by us.

When you hit ‘Deploy’ this gives you a ‘service url’. You can try this url and you’ll get a browser window with something like:


Back at the script attached to your Sheet/Form you could replace the BASE_URL with ‘service url’ (this line of code). So when someone fills in your form they will get an email to your deployed script which will have the extra bits needed in the link to issue them a badge. This isn’t great UX so instead lets see how you can use the same url in a Google Site, the advantage being you can provide additional information about collecting a badge in a Google Site. Fortunately the Google Developers site has some great guidance on Embedding your web app in Google Sites which should allow you to paste in your ‘service url’.

You can adjust the gadget settings like size and border, and of course add extra information. To see all of this in action enter your email in the form below and you should get an email with a ‘claim link’ to the ‘sandbox’ site (emails won’t be shared or used for any other purpose than sending the claim url).

Wrapping up

So if you’ve made it this far !!!WELL DONE!!! (#epic). Some things to note. This example currently only works with 0.5 spec Open Badge Assertions. Hopefully the 504 issue with Backpack/Issuer API is resolved. Some of you may have noticed that in this example I opted to use the issue_no_modal of the Issuer API rather than take full advantage of the new Apps Script IFRAME/modal mode. The reason I didn’t is there are some open issues with IFRAME and Caja’d version using no_modal Issuer API works perfectly fine (I’ve left the iframe version in the code if you prefer this way).

Enjoy and as always comments to clarify any of this are welcome