Skip to main content

Scaling Governance with Google Apps Script & the Docebo API (Thanks to ChatGPT!)

  • August 15, 2025
  • 6 replies
  • 259 views

averygrammel
Contributor III
Forum|alt.badge.img+2

When you’re managing hundreds, or even thousands, of courses in Docebo, small governance changes can feel enormous. Updating course codes, moving content into new folders, or aligning properties across multiple enablement teams might sound simple, but at scale, manual updates become a major burden.

That’s where automation comes in.

At Datadog, our Learning Technology team works closely with enablement leads to identify large-scale governance changes, then uses the Docebo API to apply updates in bulk. This removes repetitive work from our enablement partners while ensuring every change is consistent across the learner experience.

With automation, we can:

  • Set or correct course properties in bulk

  • Move content into new governance structures

  • Ensure bulk course creation starts with the right foundation

  • Maintain a full audit trail of changes

And the best part? You don’t need to be a developer to do it. 🙌

 

Why Google Apps Script?

 

Google Apps Script is a free tool built into Google Workspace that lets you write JavaScript code to automate tasks in Google Sheets, Docs, and other Google apps.

For Docebo admins, it’s a perfect bridge between your spreadsheet and the API, allowing you to make large-scale changes with a single click instead of hours of UI work.

In my case, I built a script (with help from ChatGPT!) that:

  • Reads course data directly from a Google Sheet

  • Sends it to Docebo via the API with one click

  • Logs each action back into the sheet for tracking

  • Generates a summary in Google Docs for governance records

 

Tips for Success

  • Start small → Test with 2–4 rows of varying data before running a big update.

  • Log actions → Write results back to the sheet for tracking.

  • Test in sandbox first → Never push straight to production.

  • Adapt to your environment → If you don’t have Google Workspace, alternatives I’ve tested include Postman and sending code through your computer’s terminal. You never know what tools may be available to you!

 

 

👀 Interested in Creating Your First Script?

See below for my example on how to update course properties in bulk!

 

 

Getting Started

 

1) Create your Google Sheet

List the details you want to update—like course name, code, and any property changes. Include an Environment column with values like sandbox or production so the script knows where to send the data.

2) Open Apps Script

In your sheet, go to Extensions → Apps Script.

3) Store your API tokens

Under Project Settings → Script Properties, create two properties:

  • docebo_token_production → for your live environment

  • docebo_token_sandbox → for testing safely

🔄 Important: Docebo access tokens expire after a short period (often an hour).

  • Before running the script, generate a fresh token from your Docebo environment and update these values.

  • If you’ve closed your browser or it’s been a while since you last ran the script, refresh the token first to avoid authorization errors.

 

🔐 Environment Awareness

Your sheet controls both where your data goes and which token the script uses for authorization.

  • If a row is marked sandbox, the script will use your docebo_token_sandbox.

  • If it’s marked production, it will use your docebo_token_production.

This means you can run both testing and live updates from the same script—just make sure your sheet’s Environment column is correct before clicking “Run.”

 

Add a Menu Button in Google Sheets

You can make running the script as easy as clicking a menu item:

// =====================================================
// Google Sheets script: Docebo Menu Setup
// Adds options to run course automation functions
// =====================================================
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu('Docebo Tools')
.addItem('Update Course Properties', 'updateCoursesInBulk')
.addToUi();
}

 

Once added, your sheet will show a Docebo Tools menu with an “Update Course Properties” button—no need to open the script editor to run it.

 

How to Add This Script in Google Apps Script (Step-by-Step)

  1. Open your Google Sheet
    Create or open the sheet you’ll use (with columns A–J as defined below).

  2. Open the Script Editor
    Go to Extensions → Apps Script. A new project opens.

     

  3. Name the project
    Click Untitled project at the top and give it a clear name (e.g., “Docebo Course Updater”).

  4. Paste the code

    • In Code.gs, paste the updateCoursesInBulk() code from below.

    • Below it (or in the same file), paste the onOpen() menu script.

  5. Save
    Press ⌘S / Ctrl+S (or click the disk icon).

  6. Add Script Properties (tokens)

    • Click the gear icon Project Settings (left sidebar).

    • Under Script Properties, click Add script property twice:

      • Name: docebo_token_production → Value: (your current production token)

      • Name: docebo_token_sandbox → Value: (your current sandbox token)

    • Save.

    • Remember: Update these tokens with fresh ones before running the script if they’ve expired.

    •  

  7. Reload your Sheet
    Go back to the sheet and refresh the page. You should now see a Docebo Tools menu.

  8. Authorize on first run

    • Click Docebo Tools → Update Course Properties.

    • Google will prompt for permissions. Review and allow.

    • (Apps Script uses the least scopes your code needs, like UrlFetchApp, DocumentApp, and SpreadsheetApp.)

  9. Test with 2–4 rows

    • Fill in columns A–J (start with sandbox).

    • Run the update and check the Status column and the Google Doc log link in the final alert.

  10. Move to production

  • When happy with results in sandbox, change column A to production for your target rows and run again.

 

Example Script: Update Course Properties

 

Update Course codes, thumbnails, status, etc, in bulk! 

 

Template: [MAKE A COPY] Docebo: Update Course Properties

 

 

What the script does

  • Reads columns A–J and only updates fields that have values (whitespace = blank/ignored).

  • Sends PUT to /course/v1/courses/{id} for core properties.

  • Creates a timestamped Google Doc audit log with details per row.

  • Ends with a UI alert containing a link to the log.

 

Script name: updateCoursesInBulk()
Tab name: Uploader: Update Course Properties
Environment & tokens: Uses Script Properties docebo_token_production / docebo_token_sandbox. Column A (production / sandbox) selects the base URL and which token authorizes the call.
 

 

The script

/**
* Script: updateCoursesInBulk (properties only)
* Purpose: Update core Docebo course properties from one sheet (no additional fields).
* Sheet: "Uploader: Update Course Properties"
*
* Columns (fixed):
* A=environment, B=id, C=code, D=title, E=description, F=course_status,
* G=category_id, H=Duration Hours, I=Duration Minutes, J=thumbnail_id
*
* Endpoints:
* - PUT /course/v1/courses/{courseId}
*
* Tokens (Script Properties):
* - docebo_token_production
* - docebo_token_sandbox
*/
function updateCoursesInBulk() {
const SHEET = "Uploader: Update Course Properties";
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET);
if (!sheet) return SpreadsheetApp.getUi().alert("❌ Sheet not found.");

const data = sheet.getDataRange().getValues();
const hdr = data[0];
const rows = data.slice(1);

const statusCol = hdr.length + 1;
if (!hdr[statusCol - 1]) sheet.getRange(1, statusCol).setValue("Status");

const ts = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyy-MM-dd HH:mm:ss");
const doc = DocumentApp.create("Docebo Bulk Course Update Log - " + ts);
const body = doc.getBody();
body.appendParagraph("🛠️ Docebo Bulk Course Update Log");
body.appendParagraph("🕒 " + ts + "\n");

const normEnv = v => String(v || "").toLowerCase().trim();
const norm = v => (v === null || v === undefined) ? "" : String(v).trim();

const mapStatus = (s) => {
const v = (s || "").toLowerCase().trim();
if (!v) return "";
if (v === "published") return "published";
const off = ["unpublished","under maintenance","under_maintenance","inactive","maintenance","off"];
return off.includes(v) ? "under_maintenance" : "";
};

for (let i = 0; i < rows.length; i++) {
const r = rows[i];
const rowNum = i + 2;

const environment = normEnv(r[0]);
if (!["sandbox", "production"].includes(environment)) {
const m = `⚠️ Row ${rowNum}: Invalid environment`;
body.appendParagraph(m); sheet.getRange(rowNum, statusCol).setValue(m); continue;
}

const courseId = Number(r[1]);
if (!courseId) {
const m = `⚠️ Row ${rowNum}: Missing or invalid Course ID`;
body.appendParagraph(m); sheet.getRange(rowNum, statusCol).setValue(m); continue;
}

const code = norm(r[2]);
const title = norm(r[3]);
const description = norm(r[4]);
const statusMapped = mapStatus(r[5]);
const categoryId = r[6];
const durationHours = parseInt(r[7], 10) || 0;
const durationMinutes = parseInt(r[8], 10) || 0;
const thumbnailId = r[9];

const BASE_URL = environment === "production"
? "https://YOUR_PROD_SUBDOMAIN.docebosaas.com"
: "https://YOUR_SANDBOX_SUBDOMAIN.docebosaas.com";
const TOKEN = PropertiesService.getScriptProperties().getProperty(
environment === "production" ? "docebo_token_production" : "docebo_token_sandbox"
);

const payload = {};
let hasUpdates = false;

if (code) { payload.code = code; hasUpdates = true; }
if (title) { payload.name = title; hasUpdates = true; }
if (description) { payload.description = description; hasUpdates = true; }
if (statusMapped){ payload.status = statusMapped; hasUpdates = true; }
if (categoryId) { payload.category_id = Number(categoryId); hasUpdates = true; }
if (thumbnailId) { payload.thumbnail_id = Number(thumbnailId); hasUpdates = true; }

const totalSeconds = (durationHours * 3600) + (durationMinutes * 60);
if (totalSeconds > 0) {
payload.average_completion_time = totalSeconds;
hasUpdates = true;
body.appendParagraph(`⏱️ Row ${rowNum}: average_completion_time = ${totalSeconds} seconds`);
}

if (!hasUpdates) {
const m = `⚠️ Row ${rowNum}: No fields to update`;
body.appendParagraph(m); sheet.getRange(rowNum, statusCol).setValue(m); continue;
}

try {
const res = UrlFetchApp.fetch(`${BASE_URL}/course/v1/courses/${courseId}`, {
method: "put",
contentType: "application/json",
headers: { Authorization: `Bearer ${TOKEN}` },
payload: JSON.stringify(payload),
muteHttpExceptions: true
});

const http = res.getResponseCode();
const txt = res.getContentText();

if (http !== 200) {
const m = `❌ Row ${rowNum}: Core update failed (HTTP ${http})`;
body.appendParagraph(m); if (txt) body.appendParagraph(txt);
sheet.getRange(rowNum, statusCol).setValue(m); continue;
}

body.appendParagraph(`✅ Row ${rowNum}: Core course properties updated`);
sheet.getRange(rowNum, statusCol).setValue("✅ Update successful");
Utilities.sleep(400);
} catch (e) {
const m = `❌ Row ${rowNum}: Script error during core update — ${e.message}`;
body.appendParagraph(m); sheet.getRange(rowNum, statusCol).setValue(m);
}
}

doc.saveAndClose();
SpreadsheetApp.getUi().alert(`✅ Update complete!\n\n📄 Log: ${doc.getUrl()}`);
}

 

 

 

Want to Build More Scripts?

 

You can use this guide and script as a baseline for almost any Docebo API automation.
Simply copy the script into your prompt for ChatGPT and say something like:

“Here’s my current Google Apps Script for Docebo. Please adapt it so it works with this (provide api endpoint) API endpoint to archive enrollments, keeping the same environment selection, logging, and sheet structure.”

ChatGPT can then help you:

  • Swap in new API endpoints

  • Adjust the payload structure

  • Add or remove columns in your sheet

  • Keep the same environment-awareness, logging, and governance structure

This way, you can create scripts for:

  • Bulk enrollments or unenrollments

  • User updates

  • Catalog changes

  • Reporting exports

 

 

👏 Once you understand the flow, the automation possibilities are endless!

6 replies

dklinger
Hero III
Forum|alt.badge.img+11
  • Hero III
  • August 15, 2025

You are so freaking awesome ​@averygrammel.

This is what we were talking about after I heard your curation discussion up in the Boston meetup. 
Things like this is what makes the difference.

All kinds of heart emojis - thank you!!!!! ❤️❤️❤️❤️❤️


dwilburn
Guide III
Forum|alt.badge.img+4
  • Guide III
  • August 15, 2025

Thank you ​@averygrammel! I attended the session but was very much interested in more details. Now I have something to read while I eat lunch.


  • Newcomer
  • August 16, 2025

I tried but it fails. Here is the error from the log:
 

🛠️ Docebo Bulk Course Update Log

🕒 2025-08-17 00:46:50
 

❌ Row 2: Core update failed (HTTP 500)

<html>

<head><title>500 Internal Server Error</title></head>

<body>

<center><h1>500 Internal Server Error</h1></center>

</body>

</html>


averygrammel
Contributor III
Forum|alt.badge.img+2
  • Author
  • Contributor III
  • August 18, 2025

Thank you so much ​@dklinger & ​@dwilburn !! 😊 Glad to be able to get this information out in a way for folks to be able to go back to as well.

 

@vtiwari - Thank you so much for flagging that! I’ve just left this note right able the script for others as well, that there was one step that I missed calling out in this “template” version. 

Replace the placeholder domains in the script with your actual Docebo tenant URLs (e.g., https://datadog.docebosaas.com and https://datadogsandbox.docebosaas.com). If your company uses a custom domain, use that instead. 

  • "https://YOUR_PROD_SUBDOMAIN.docebosaas.com" :
  • "https://YOUR_SANDBOX_SUBDOMAIN.docebosaas.com";"

  • Newcomer
  • August 18, 2025

Hi ​@averygrammel, thank you for assisting with the details! I did actually change that as the first thing and brought my custom domain to ensure that the endpoint is the right token bearer. Gemini was great in assisting with these. However, I realize that the same error persisted with not just this but even another appscript I had with a simple GET request. I guess something else needs to be looked at. I’ve opened a ticket with Docebo.


averygrammel
Contributor III
Forum|alt.badge.img+2
  • Author
  • Contributor III
  • August 18, 2025

No problem ​@vtiwari , so happy you are trying it out! The Docebo Support team is the best as well!