Showing results for 
Show  only  | Search instead for 
Did you mean: 

Bulk Publish / Delete Pages

Community Champion
29 41 15.9K


This Google Sheet will allow teachers to bulk publish, unpublish, or delete their wiki pages from a single spreadsheet page.

Canvas has created their own solution to bulk deleting pages that will be available in the August 15, 2020, release. More information is available in the Canvas Release Notes (2020-08-15) 


There have been a couple of feature requests to allow for bulk deletion of courses:

  • (Sep-Dec 2015, ended with 21 votes)
  •   (Jan - Apr 2016, currently open for voting)

The main complaint behind these requests is that you have to pull up the list of pages, click on the admin cog and choose Delete, then confirm that you want to Delete the page. That takes three clicks for each page not counting the time you have to wait for the page to be deleted. To a lesser extent, there was once a need to publish/unpublish courses, but that has been nicely rolled into the interface. You just need to click once per page to publish or unpublish.

Although this isn't a very popular idea and won't save a lot of time if you only have a few pages, it's not a terribly bad idea, either, and may benefit someone who needs to delete lots of pages.

One of the things I like to do is write code that regular users can use while they wait on features to be implemented; I call them Canvancements. I've been writing a bunch of Google Sheet applications for Canvas recently, and so when I was the message come through my Inbox today, I thought it wouldn't take long, as it was essentially the same process I was using with the manage all due dates from a single spreadsheet that I wrote back in August and updated January 9, 2016.


One Time Setup

  1. Log into your Google Drive account.
  2. Open the Page Manager spreadsheet.
  3. Choose File > Make a copy. This won't be available if you're not logged into Google Drive.
  4. There will be a Canvas menu added to the Google Sheet and you'll use it for everything else. From that menu, choose Configure API settings. You'll need to authorize Google to access your Canvas instance and tell it what your instance and access token are.

Those are things that need done before you can use the spreadsheet.

Regular Use

  1. Choose Select Course from the Canvas menu in your Google spreadsheet. Enter the Canvas Course ID as a number or you may also just paste a URL from your course.
  2. Choose Load Page List from the Canvas menu. This will load a list of the page titles and URLs and display it along with their current published state (1: published, 0: unpublished).
  3. Edit the Action column for the pages you want to change. You don't need to do anything with the pages you don't want changed.
    • Use a 1 to publish a page
    • Use a 0 to unpublish a page
    • Use a -1 to delete a page
  4. Choose Save Changes from the Canvas menu to write the changes to Canvas.

The next time you need to use this, just choose a new course and start from there.

Video Walk-Through

I consider this one of the simpler interfaces to explain, so I made a video for you. If that sounds backwards, realize it's about time management -- simpler means it takes less time to explain and edit.


  • The need to publish or unpublish may not be as great, but I it was easy to do and worked out well with the design so I included it. Besides, some people may want to delete all unpublished pages, so it serves as an easy way to identify those pages (hint, do a sort by action to group them and make them easier to change).
  • The video only shows changing two pages for demonstration purposes, but you can change as many as you need to change.
  • Do not move the columns around or delete them. I wasn't as flexible with this one as I was with the due dates spreadsheet. This one assumes you have an action, followed by a title, followed by the URL.
  • You may delete rows if you like as it does not affect the process. To be clear, you do not delete a row from the spreadsheet to delete it from Canvas, you put a -1 (negative one) in the action column to delete a page.
  • My spreadsheet apps have come a long way since I first started writing them. Everything is now controlled through a Canvas menu that is added to the Google Sheets menu. You can copy/paste your information there and Google will save it for you. You don't have to worry about sharing the document with someone because the credentials you don't go with it. When specifying Canvas IDs, you can paste an appropriate URL and it will grab the information for you. There's no longer a need to edit the source code like my first scripts required. Finally, some errors are now logged to a pop-up alert box so that you don't have to go into the source code and view the log files when things don't go as planned.
New Member


I had a Canvas "sandbox" course that had accumulated almost 500 pages (don't ask). Because of your clear, quick, and easy process, I was finally able to delete them! Thanks so much!

Community Champion


I'm glad you found it useful. It's that kind of use-story that makes development worthwhile.


James, this is awesome! I am always looking for tools to share with my institutions using Canvas. Do you have a reference to the other google sheets you have created? Do you have one for publishing courses, or bulk deleting assignments? I always hear the request for bulk publishing of courses, and I admit I have many 'test assignments' I'd like to mass delete. Thanks!!


Community Champion

angela, Some but not of my stuff can be found on my Canvancement site on GitHub. There are some that just buried inside feature requests here in the Community, like the one to manage all the assignment due dates from a single page. I've got a count all discussions spreadsheet and am very nearly done with a one that that will give a much more detailed analysis for a single analysis, including who did the liking and what messages went unread (you need masquerading privileges for those last two, so it may not be as useful as it sounds) and I've been using it this semester to help grade my discussions. There's nothing written up on it as it's still in development.

There is another one that allows you to generate a list of courses that are unpublished and allow you to publish them. I don't have anything like that for assignments.

But I don't have a list of where all these things are other than to click on my name in the community and look at my content. That won't help with stuff I originated, but maybe  @kona ​ or some of the other people who have them bookmarked can help generate a list.

Community Coach
Community Coach

angela​, here's what I've got on my list of amazing  @James ​ creations:

In addition, if you go to James profile page in the Community (either by searching for him or by clicking on his name from anywhere in the Community) you can click on "Content" and then "Blog Posts" and then "Documents" to see everything James has published like this.

Hope this helps!

Community Champion

Thanks for compiling that list  @kona ​. It's not that I refuse to create a document for the adjusting all due dates, it's just that doing it right involves multiple spreadsheet recipes (and videos) like I made for the Obtaining and using Access Report data for an entire course​ and I've got more pressing issues.

Community Champion

 @James ​ - Thank you for creating these resources, they are a great help.

I have a test course that I am trying to bulk delete pages from, and I am successful up until the point where I try to Save Changes. I get this message:


Any ideas?

Community Champion

This must have been a script I threw together in a hurry, I noticed that I still refer to "Dates" as the sheet name instead of "Pages". There's also a typo on a variable, but it turns out to not be used anywhere, so it was a double mistake that doesn't affect things. I think I might have intended to add editing the page title, but the URL is based off the title and I wasn't sure what would happen if you had a page called "Ernest Hemingway" but with an URL of "f-scott-fitzgerald", so I decided to leave that part to Canvas.

Moving on ... I just tried the script and it deleted pages as well as published and unpublished. Since it's working on my end, let's look at things you can try to help narrow down the problem and then maybe I can fix it if needed.

My guess is that it doesn't like the URL of one of your pages.

Let me start with an easy question before I get into the complicated.  You're not "blanking" out pages you don't want to change by putting spaces in there or otherwise modifying the URL for a page are you? Or perhaps trying to change the name of the page and the URL using this program?  If so -- don't. The error your getting happens when there is something in the URL column of the spreadsheet that isn't found in Canvas. "blanking" the row or deleting just the URL without the entire row will leave an entry and then it can't find the page in Canvas and the error would be thrown. Changing the name isn't supported for the reason I gave in the first paragraph.

By the way, you don't have to do anything with the pages you don't want modified, it only modifies the the things that have changed. But if you do want them completely gone, then use highlight the entire row (click on the number on the left) and then right-click and choose delete row.

Okay, now for some troubleshooting in case that wasn't the issue.

If you're okay making some simple code changes, we can try to figure out which one.

Go to Tools >> Script Editor


Click on on the left.


Go down to line 145 (this may be slightly different, but it should be close) and find the line that has the definition for title. This is the typo that normally isn't a problem, but we're going to use it to track down the problem.
Change it from rows[i][0], which is the status, to rows[i][1], which is the title.

Then, change the '; at the end of line 148 (the really long line with the error message), to be

\n Row ' + i + ' : ' + title before the semicolon.


This should tell you the page number and the title. Note that even though it says row number, it's zero based, so it won't match what's on the spreadsheet. In other words, if it says Row 3, it's the 3rd page, which is in the 4th row of the spreadsheet because of the headers. You could put (1+i) instead of i if you wanted to match. This is basically for testing purposes.

If it gives you an error with the title, then remove the part about the title at the end. This might happen if you have what looks like blank lines somewhere.

Community Champion

Success! The code change did indeed help me figure out what the problem was. I have a lot of pages copied from someone else's course. The pages were named according to chapter and section, for example "7-1 Exponential Notation". But each chapter also has an introductory page titled simply (for example) "7-1". The troubleshooting code revealed that spreadsheet was translating those pages as dates. So instead of looking for a URL ending in "7-1" it was looking for a URL ending "Sat Jul 01 2017". [insert eye roll]. So, it's just a case of the spreadsheet trying to be smarter than we want it to be. A cautionary tale for anyone with numbers and special characters in their page names.

Community Champion

Those pesky dates! I'm glad you figured it out.

Community Contributor

 @James ‌ can this (or other tools you have created) be used to duplicate pages? For example, I have templates for many of the pages I create. Have you developed any tools that would allow me to do this?

Community Champion

Short answer - no.

The desire to copy things is often echoed in the Community. In particular, templates have come up  and been placed on Product Radar. Although I haven't looked at it in several years, I keep hearing USU Design Tools (aka KennethWare) thrown around and it has a template manager feature. So, if you're looking specifically for making templates, that might be something to pursue, rather than copying pages and then editing.

Every time I've looked into it, it's not as simple as it seems. That said, pages should be one of the easier things to copy -- provided you can figure out a naming scheme. Pages are referred to by the URL which is based off the page title. This means that you couldn't duplicate the page without changing the name, unlike some other things where you could technically, but confusingly, have two assignments or two quizzes with the same name.

You could adopt the scheme of adding a sequential number to the end of the page name. That should work unless there's already a page with that name, so you'd need to watch out for that issue. Then the user could go through and rename the pages one at a time.

I am pretty sure I have programmatically created pages using the API through PHP (I know I've done it for assignments). I've also updated all of my lecture notes to add a menu template to the existing page. The other thing I've done is very low-tech: that's to edit one page using the HTML editor and selecting all the content. Then create the new page, switch to the HTML editor, and paste. Repeat as needed. That works pretty well when the number of pages is small, but it might be easier in those cases than messing with the overhead of a more powerful tool.

Community Champion

Thank you so much,  @James ‌!  You never cease to amaze me.  Works like a charm.

Community Member

Hi James, I'm a new fan! Wish I'd discovered all of your helpful "Canvancements" earlier!

I was wondering if you had any ideas regarding the scheduled publishing of Front Pages. We had this in our old LMS. I'd love a feature where you could delay the posting of multiple Front Pages so they would swap out every week or so. Would love to hear your feedback on this.

I've submitted the idea for it so if anyone wants to vote it up please do!

Thanks for everything you do James, you're heroic efforts are much appreciated!

Community Champion

I have had ideas and I've shared them in the past. I actually found your other post before I saw this one, so I was just thinking about them. I'm going to comment there rather than hijacking this thread.

Community Member

Hi James,

Thanks for the very helpful script. I modified it to update module item prerequisites and it works great! However, since we have many courses, it takes a while to go through each module and each of its module items to specify the prerequisite, my script times out after updating about 4 courses. I added a column to track which ones have been done and then run the script again from that point on. It will take hours to update prerequisites for all our courses. Do you have any tips to shorten the update time or any work-around of google script's timeout?

Thanks so much for your wonderful insights!

Community Champion

 @nschutz ,

Don't use Google Sheets if possible. It is synchronous, so each API call has to finish before the next one begins. If you use an asynchronous system that will handle concurrent requests you can make them much faster and since it's a program not using Google Sheets, it can run for more than 5 minutes. If you need Google Sheets for the editing capability, then save the results to a text file that you can then read that with another program to do the actual processing.

I'm not sure what you're trying to accomplish exactly or what calls you're making so it's hard to tell if there is a better way to accomplish it through different calls or more optimal ways to do things.

With Google Sheets, you're limited to the API. If you're using JavaScript, then it's something that you might be able to add into the browser and then run from there. I see about 5 concurrent requests that way. I wrote something the other day that made 1400+ requests in less than a minute. Even if you don't add a button to start that process, you can usually copy/paste the JavaScript into the browser's developer console and run it that way.There are sometimes internal AJAX calls that allow you to send more information at once than you can with the API.

If you absolutely must use Google Sheets, it may be possible to cache some of the information to reduce calls between runs.

Community Member

I will try the javascript idea, however, I still have a concern. In the Google script, each API call is checked before the next API call, see below.

      var res = canvasAPI('PUT xxxxxxxxxxx', {
        ':course_id' : courseIds[i]
      if (typeof res === 'undefined') {
        throw('Error xxxx');

In other words, the synchronization in enforced from the script, not API, correct? Do I remove the if statement so multiple API calls can be made since the script will run faster than the API? I don't believe the PUT calls I need to make allows for batch mode.

You mentioned you can see how many concurrent calls running, is it quick and easy for you to explain how you do that?

Thanks again for your time.

Community Champion

 @nschutz ,

Thanks for pushing the question -- I learned some new things this morning that I may be able to incorporate. Unfortunately with the distribution system of Google Sheets, it means people would have to redownload the Sheet and redo their access tokens. Since Google has now implemented more rigorous verification, that may be more challenging (the old scripts were grandfathered in).

You may make multiple simultaneous requests to the Canvas API. Canvas has rate limiting in effect so if you ask for too much information too quickly, it will stop the processing. There's not a fixed number, it varies on server load and how much information you're asking for and what works one minute may not work 10 minutes later. When making one connection, waiting for it to finish, and then continuing, you will not run into a problem. The 5 or 6 that I see in a browser is by opening the browser's developer tools and looking at the network panel. I think Chrome has a default maximum of 6 concurrent connections to a host. That makes it play nice with servers.

The issue with Google Sheets and Google Apps Script is that the fetch() method is blocking. This means that it will not proceed until it has a response for you. It sits there and waits. It is not my if{} statement that causes the problem, it never gets to the if{} until Google has gotten a response from the fetch(). I do, however, make sure that if there are paginated results that you have all of it available before it returns to your code. I wouldn't have to do that, but when I was writing my first Google Sheets and the API, I was coming from a PERL and PHP background and they work in similar ways.

Other languages, like JavaScript in a browser, use an asynchronous non-blocking fetch(). This is similar to an email, where a person sends off an email and then goes and does other things and kind of forgets that they sent it. At some point, hopefully, a response comes back and then the person opens the email, reads it, and does something based on the contents. The asynchronous part means that the responses may not come back in the order that you sent them -- if you send emails to five friends, there's no guarantee on the order they will be received in.

When fetching data with a GET, you may need to be concerned about getting all of the data returned by pagination before processing it. At least the way I wrote it, it does. The headers contain the link header, with some of first, current, next and last link headers being available. If there is a last link header, then it is possible that you may be able make a reasonable guess about what all of the URLs between current and last are going to be, automatically generate them, and make them in parallel. I've started doing that with some of my newer code. If there is pagination with no last header, like page_views, then you will have to wait for each request to finish to see what the next link header is and then make that call. That's the officially supported approach for pagination, but it doesn't allow for concurrent requests.

However, if you are making PUT or POST statements, you do not need for one to finish to see what the next one is, there isn't a next response to fetch. That means that you can issue a bunch of these as fast as you can as long as you don't exceed the rate limits from Canvas. The browser normally keeps that from happening.

You cannot do that with the Google Apps Script fetch() method. However, there is a fetchAll() method that allows you to send multiple requests at the same time.

I did some testing by loading the first 30 pages worth of courses (10 at a time) from our Canvas instance. I did them one at a time using the fetch() method and it took 17.123 seconds. I repeated it two more times, just in case there was caching done that may make it go faster. The next two times were 10.614 and 9.618 seconds.

Then I used the fetchAll() method. One call with 30 requests in it took 0.764 seconds to fetch the same data that took at least 10 seconds with fetch(). That's not a typo. In fact, that's what made me think that caching might be going on and caused me to retest the single one. I retested this one again and got 0.696 seconds.

Now that I had it working, I thought I would do a more robust test and get 3000 courses (300 requests of 10 each). Google Application Scripts must send all of the requests simultaneously, because it threw an error and said "403 Forbidden (Rate Limit Exceeded)". At least I know what happens when you exceed the rate limit. It stops. I tried 100 requests with the same result and 50 requests with the same result. Yet 30 requests made it through. Remember though my warning about there not being a fixed limit, it's based on the number returned by Canvas in the headers, so I might try it again with 50 and it work or again with 30 and it not.

I switched it up to 30 requests of 100 courses at a time. This worked just fine and now took 7.174 seconds with the fetchAll() method and 24.457 seconds with the fetch() method immediately following that.

I would definitely say that if you want to use Google Sheets, then the fetchAll() method has great potential in your case. Don't be too ambitious in how many requests you make at a time, though. It will attempt to make all of them at once, which may push your rate limit over and stop you. However, it will block on each call, so that the results of each fetchAll() are finished before it makes another. If you put 5-6 in there at a time, you would basically be pretending to be a browser. You can probably bump that number up a little and be safe, but it depends on the calls that are being made and the server load.

Here's the code I used for testing. The XXX and YYY need replaced before it will run. Change line 15 to if(1) to run the fetch() and if(0) to run fetchAll(). I went ahead and generated all of the requests both ways just so that was a common factor in the timing.

function speedTest() {
  var host = '';
  var token = 'YYY';
  var parms = { 'headers' : { 'Authorization' : 'Bearer ' + token , 'Content-Type' : 'application/json'} };
  var url = 'https://' + host + '/api/v1/accounts/self/courses';
  var paginatedUrl;
  var urls = [];
  var requests = [];
  for (var i = 0; i < 30 ; i++) {
    paginatedUrl = url + '?page=' + (i+1);
    requests.push({'url' : paginatedUrl, 'headers' : parms.headers});
  if (0) {
    for (var j = 0; j < urls.length; j++) {
      var response = UrlFetchApp.fetch(urls[j], parms);
  else {
    var responses = UrlFetchApp.fetchAll(requests);
  Logger.log('All done');
Community Member

Hi James,

Read your message multiple times and so thankful to you for sharing all of this! I decided to try the fetchAll approach in Google Script first because I am more than half way there, but got deeply stuck. (Sigh...) I put all the update (PUT) requests in an array, and send them through to fetchAll 20 at a time. It turns out that only some of the requests got executed and each time I ran the function, the ones that get executed are different. Below is my function and request object to make the fetchAll calls.

function batchAPICalls(requests) {

  var responses;
  var subset = [];
  var length = requests.length;
  try {
    while (length > 0) {

      for (var j = 0; j < 20; j++){

      responses = UrlFetchApp.fetchAll(subset);
      if ( typeof responses === 'undefined' || responses.length == 0){
        responses = null;
        requests.length = 0;
        return responses;
      requests.splice(0, 20);
      subset.length = 0;
      length = requests.length;
  } catch (e) {
  return responses;

My request object looks like this.

      var request = {
         'url': url,
         'method' : parms.method,
         'headers' : parms.headers,

Do you see something obviously wrong? Is this a timing issue?

Community Member

P.S. I changed the subset to 5 requests at a time, and still get the same problems, some of the PUT calls are not executed. Obviously, when I changed the subset to 1, then it all worked but I am back to square one.

Community Champion

The fetchAll() method returns an array, so the check for success is always going to pass. Even if it is successful, you're only going to be returning the responses from the last fetchAll(), not all of them. In other words, don't use this for a GET if you have more than can be received in a single call.

You may benefit by going into the Script Editor and debugging and looking at the responses object to see what is failing and what the message is. I would add a breakpoint right before the fetchAll() and then start stepping through things to see what happens. There's also a muteHttpExceptions that causes it to not throw an error when something fails. Since that defaults to false, if you're not getting an error (I see a big red error message at the top of the page), then it's not generating an error (like a 403 unauthorized) but there may be something else in the request that's not working.

Sometimes you can get a sense of what is happening by looking at the Execution Script and to a lesser extent by a liberal use of Logger.log() and then looking at the log (Ctrl-Enter I think).

I started playing around with the fetchAll() after our last conversation and started rewriting my modules to make use of it. Then I got pulled back in to my full-time job (teacher) and haven't gotten back to it. What I was looking at was speeding up the GET calls, though, not so much the POST or PUT, but I was writing my code generically enough that it could be used for either. I haven't tested it, I'm not even developing it in Google Sheets at this point, so it's not in a ready-to-share state.

Community Champion

Awesome work (again) James.

More attention needs to be given to the use and benefits of pages in Canvas. Got to move away from making courses endless repositories of text links. So important for K12.

Thanks again for all your time and effort, freely given, to support the community..

Community Member

I did more logging like you suggested, logging each request and response object. It shows that all requests were executed and all the matching response object were returned with a status code of 200. Nothing failed. I am wondering if there's a problem on Canvas backend not able to handle async requests?

I will try the javascript approach, and hope it works.

Community Champion

The problem isn't asynchronous, as JavaScript, which Google App Script is very closely related to, is asynchronous. Going JavaScript through a browser is going to limit the maximum number of simultaneous connections, but otherwise very little will change as far as Canvas is concerned.

I was able to send a post command through the JavaScript Web fetch() command using a Promise.all construct, which essentially sends as many as it can allow at one time and then waits for them to finish. That's pretty close to the Google fetchAll() method except that the one within the browser was limited to 6 connections, despite me sending 11-12 at a time. Google App Scripts seems to send more than that at a time, but you said you were limiting it to 5 and it still didn't work. It may be related to the number of simultaneous calls or the processing cost of what you're trying to do. My calls were not to the API, but an internal undocumented endpoint, so there might be some limitation on the API that I haven't really tested.

Are you seeing the failure trying to send one batch of 20 (or 5) or when you send a bunch of those requests? By default, Google makes those calls synchronous and it waits for one to finish before sending a second (I think you can change that), but even if it's not, then sending 5, waiting for it to complete, and then sending another 5 right away may have not provided enough time to recover from the first 5. If it's failing when you're sending more than a single batch in quick succession, then it's likely that you're hitting the limit on the API and you need to monitor it to make sure that you're not going over. If it fails on a single batch of 5, then it's unlikely to be the throttling, but it may be.

Make sure it works for a single fetchAll() and look at the response headers to see what the X-Rate-Limit-Remaining is. It starts at 700 and if it gets to 0, it stops accepting requests. See the page in the API documentation on Throttling for more information. It mentions parallel requests being subject to an additional penalty, so it may be that 5 are too many.

There are some logic errors in your code that will fail at some point when you don't send the full 20 (or 5), but I didn't address that since I figured you were still testing.

Still, going JavaScript or some other language and just letting it run in the background at a slower pace may be a better solution because of the 5 minute timeout with Google Sheets. By the time you slow your script down enough that it will run, you may be back up against that 5 minute limit.

Community Champion

I just stumbled across another reason you may not be able to use Google Sheets for a super-intensive API task. They have quotas on how many URLs you can fetch a day.

Quotas for Google Services  |  Apps Script  |  Google Developers  and Google Apps Script Dashboard - Google Drive 

The current limit is 20,000 fetches for the free consumer version. But there are also limits on how much information can be transferred with those (both directions) as well. I don't think you're hitting those limits yet, but it's something to consider if you're a heavy user of Google Sheets with Canvas.

Community Member

Throttling seems to explain what's happening as I am sending over 150 PUT requests in batches of 5. What's confusing to me is that when the response objects come back from the fetchAll, it appears the requests have been completed but they indeed may not have, even though they all have a status code of 200? I looked at a request that was not executed in the fetchAll but the response object shows it was. I will give javascript a try soon.

Yes, I did correct the logic error, but thanks anyway! You are simply amazing...

New Member

Thanks for this, James! I'm a bit new to Canvas, as we just migrated from D2L/Brightspace. I now have a migrated course with many pages I need to delete, and found this thread as a result. The only problem I have is that when I make a copy of your Google Sheet, I don't see the "Canvas" tab/option appearing. Any ideas on what might be causing that? I believe I have properly signed into Google Drive and have linked my Canvas to Google Drive as well, but I'm guessing there's something I'm missing. Thanks in advance for any help you might be able to suggest!

Community Champion

In my experience, it can take up to a minute for that Canvas menu option to appear in my copy. You might try to pull it up again and wait about that long to see if it appears.

New Member

Thanks for that heads-up. Unfortunately, that doesn't seem to be the issue in my case. Likely operator error--just still can't quite determine what would be causing my Canvas/Google Drive accounts to not be fully synching for this sheet.

Community Champion


I'm going to list some things that you have probably already checked, but I want to be complete.

  1. Make sure that you're using Google Chrome. I don't think this would keep the Canvas menu from appearing, but Chrome works better with Google Docs than Firefox and IE/Edge have major issues.
  2. Make sure that you're using an account that ends in Some schools lock down settings that content or users from external sites from interacting with scripts or files. I see this in Canvas as well when a student thinks they know more than me and it's okay for them to use their high school email address -- until the collaboration doesn't work because their school has locked out other users.
  3. Make sure that you're using your copy. After going File > Make a Copy, you need to close my copy and make sure you're using the copy that was created. My copy will never show the Canvas menu for you since it's read-only access for you.
  4. If all that fails, go to Tools > Script Editor and make sure my code is there. It should open to a file and also have some others listed on the side like, canvasConfig.html, help.html, and If that's not there then there is a bigger issue at hand, one that will take more time to research.

Google has been implementing some security changes to make it harder to authorize scripts, but I believe that comes after you try to run the script from the menu (not 100% positive on that). It's possible that there's some new setting that keeps it from running. I suspect that my scripts work because they're grandfathered in, but there may be a date where they stop that from working. With other people, it's just been an extra step to get it authorized.

Finally, linking your Google Drive to Canvas has absolutely nothing to do with this script working or not working. This script is completely external to Canvas and doesn't recognize any integrations like that. It uses the Canvas API instead to communicate.

New Member

James--thanks so much for the detailed and quick reply! I believe the issue here was #2--my institution normally connects with a gmail account that has our institution name after the @ sign rather than Once I connected Canvas with my personal account and then ran the Page Manager file from that Google Drive Account, the Canvas Menu popped up, after a delay of just a few seconds as Matt Hanes mentioned might happen.

After that, it was as simple as following the steps in your video--it appears to have worked perfectly. My course now has just over 500 pages, instead of the approximately 1100 it had before (there was a hidden, duplicate second version of an older iteration of the course when it was migrated over). So this application probably saved me literally hours (days?) of clicking to delete those. Thanks again for all of your help!

Community Champion

I'm glad the solution was simple and that you saved a bunch of time.

Community Member


I am wondering if you have already figured out an alternative when the google script stop working? I use that quite a bit right now and don't want to be caught off guard. I like how the Google sheets provide a easy way to provide the input data for the scripts.

Thanks again for your much appreciated insights.


Community Champion

 @nschutz ,

This is not an encouraging answer, but I have not looked at it since the last we chatted and with classes starting next week and my preparation for them sitting at about 3.5%, I won't be able to take up the cause any time soon. I have no new solutions beyond ones that we have talked about before.

Community Member

No worries. It's good to know. :smileygrin:

Hope school prep goes well for you.

New Member

Hi, great work! I was working on a beast migration with 300+ pages and this helped immensely.

The script helped with unpublishing the pages, but I'm wondering if there's a tool to unpublish multiple files? This seems to miss out the files.

Community Champion


I know that at least one person has taken my Google Sheet that adjusts the due dates on assignments and modified it to work with files. I have not used it myself since I don't change files much: 

This script is specific to pages. Pages, Files, Discussions, Assignments, Quizzes, Announcements all have different API calls and are handled differently within Canvas. Writing a script that handled everything would be overkill for what most people want and would take too long to write. Writing small scripts that handle one thing are easier to knock out, but then there are people who don't get to use it. Similarly, Canvas doesn't try to add every feature that could be developed. Instead, it tries to keep the interface simple and intuitive and do a great job at what it does do.

New Member

Can we use this same spreadsheet for bulk deleting quizzes? I have about 100 quizzes I need to delete.

Community Champion

 @calee3 ,

This particular script only supports content pages and will not delete assignments or quizzes.

You could use it as a foundation for a script that would delete quizzes, but if there's only 100, it's going to be quicker to delete them manually than write the script.

New Member

Seriously. You have saved me SOOO much time! Right from incorrectly pushing out 250 pages which I could undo with your script. To unpublishing thousands of pages across my institution. Legend!