Skip to content
← Home

Articulate JavaScript Tools

JavaScript scripts for Articulate Storyline 360 and Rise 360. View on GitHub

JavaScriptArticulate Storyline 360SCORMStoryline 360 Scripts

JS Trigger: Progress Bar and Slide Count

GitHub

Adds a progress bar with percentage text and slide counter to the Storyline 360 player top bar.

v1.0.0Slide MasterPlayer Top BarDOM Injection

What It Does

Adds a progress bar with percentage text and a slide counter to the Storyline 360 player top bar. Three elements appear side by side: a "Course Progress: XX%" label, a visual bar that fills and changes color at 100%, and a "Slide: X/Y" counter. On-slide progress indicators are no longer needed, which frees up the full slide area for content.

How It Works

On each slide load the script checks whether the progress bar DOM elements already exist in the player links-right container. If they do not exist (first slide), it builds the full widget. If they do exist (subsequent slides), it updates the values in place. A setInterval runs every 500 ms to stay in sync if variables change mid-slide. Two short setTimeout calls at 150 ms and 400 ms catch cases where Storyline updates variables slightly after the timeline starts.

Setup

  1. Open your Storyline project, go to View, then Slide Master, and select the top-level master slide (the parent, not a child layout).
  2. Create a variable named Progress (capital P, case-sensitive).
  3. Add a trigger: Adjust Variable, set Progress equal to Menu.Progress or Project.Progress.
  4. Add a second trigger: Execute JavaScript, paste the contents of the script.
  5. If your player has no right-side buttons such as Resources or Glossary, add a placeholder character to a player tab to keep the links-right container visible.

Configurable Variables

VariableDefaultDescription
bgColor#F6F9FBTrack background color
barColor#FCCE4BProgress fill color
compColor#19BB32Fill color at 100%
borderRad100pxBorder radius for rounded ends
barWidth220pxWidth of the progress bar
barHeight15pxHeight of the progress bar
wrapperX / wrapperY20px / 0pxOffset of the entire widget
barX200pxLeft offset of the bar
slideTextX440pxLeft offset of the slide counter

Slide Counter

The script reads MenuSection.SlideNumber and MenuSection.TotalSlides (Storyline built-ins) first. If those are not readable via JavaScript, it falls back to user-created variables named CurrentSlide and TotalSlides. If neither source is available, the counter shows Slide: --/--.

/**
 * JavaScript Trigger Progress Bar Slide Count in Storyline
 *
 * Purpose:  Adds a progress bar with percentage text and a
 *           slide counter to the Storyline 360 player top bar.
 *
 * Author:   Joseph Black  |  Version: 1.0.0
 * Software: Articulate Storyline 360 x64 v3.113.36519.0
 */

/* ── USER-CONFIGURABLE VARIABLES ─────────────────────────────────── */

const container   = document.getElementById("links-right");
const bgColor     = "#F6F9FB";
const barColor    = "#FCCE4B";
const compColor   = "#19BB32";
const borderRad   = "100px";
const progressVar = "Progress";

const fallbackCurrentSlideVar = "CurrentSlide";
const fallbackTotalSlidesVar  = "TotalSlides";

const wrapperX          = "20px";
const wrapperY          = "0px";
const progressTextWidth = "190px";
const barWidth          = "220px";
const barHeight         = "15px";
const slideTextWidth    = "95px";
const barX              = "200px";
const slideTextX        = "440px";

/* ── INTERNAL LOGIC ──────────────────────────────────────────────── */

const player = GetPlayer();

function getRawVar(varName) {
  try { return player.GetVar(varName); } catch (e) { return null; }
}

function getNumVar(varName, fallback) {
  const num = Number(getRawVar(varName));
  return Number.isFinite(num) ? num : fallback;
}

function getProgressValue() {
  return Math.max(0, Math.min(100, getNumVar(progressVar, 0)));
}

function getSlideValues() {
  const builtInCurrent = getNumVar("MenuSection.SlideNumber", NaN);
  const builtInTotal   = getNumVar("MenuSection.TotalSlides", NaN);
  if (Number.isFinite(builtInCurrent) && Number.isFinite(builtInTotal) && builtInTotal > 0) {
    return { current: builtInCurrent, total: builtInTotal, source: "built-in" };
  }
  const fbCurrent = getNumVar(fallbackCurrentSlideVar, NaN);
  const fbTotal   = getNumVar(fallbackTotalSlidesVar, NaN);
  if (Number.isFinite(fbCurrent) && Number.isFinite(fbTotal) && fbTotal > 0) {
    return { current: fbCurrent, total: fbTotal, source: "fallback" };
  }
  return { current: null, total: null, source: "none" };
}

function ensureUI() {
  let progressBarContainer = document.getElementById("progressBarContainer");
  if (!progressBarContainer) {
    progressBarContainer = document.createElement("div");
    progressBarContainer.id = "progressBarContainer";
    progressBarContainer.style.cssText =
      `height:20px;position:relative;transform:translate(${wrapperX},${wrapperY});white-space:nowrap;`;

    const progressBarText = document.createElement("span");
    progressBarText.id = "progressBarText";
    progressBarText.style.cssText =
      `position:absolute;left:0;top:0;width:${progressTextWidth};text-align:right;font-size:15px;font-weight:600;color:white;`;

    const progressBarHolder = document.createElement("div");
    progressBarHolder.id = "progressBarHolder";
    progressBarHolder.style.cssText =
      `position:absolute;left:${barX};top:3px;width:${barWidth};height:${barHeight};`;

    const bgBar = document.createElement("div");
    bgBar.id = "bgBar";
    bgBar.style.cssText =
      `position:absolute;left:0;top:0;width:100%;height:100%;background:${bgColor};border-radius:${borderRad};`;

    const pBar = document.createElement("div");
    pBar.id = "pBar";
    pBar.style.cssText =
      `position:absolute;left:0;top:0;height:100%;border-radius:${borderRad};`;

    const slideCounterText = document.createElement("span");
    slideCounterText.id = "slideCounterText";
    slideCounterText.style.cssText =
      `position:absolute;left:${slideTextX};top:0;width:${slideTextWidth};text-align:left;font-size:15px;font-weight:600;color:white;`;

    progressBarHolder.appendChild(bgBar);
    progressBarHolder.appendChild(pBar);
    progressBarContainer.appendChild(progressBarText);
    progressBarContainer.appendChild(progressBarHolder);
    progressBarContainer.appendChild(slideCounterText);
    container.appendChild(progressBarContainer);

    if (window.getComputedStyle(container).display === "none") {
      container.style.display = "block";
    }
  }
}

function updateUI() {
  const progressValue = getProgressValue();
  const slideValues   = getSlideValues();

  const progressBarText  = document.getElementById("progressBarText");
  const slideCounterText = document.getElementById("slideCounterText");
  const pBar             = document.getElementById("pBar");

  if (progressBarText) progressBarText.textContent = "Course Progress: " + progressValue + "%";
  if (slideCounterText) {
    slideCounterText.textContent = slideValues.current !== null
      ? "Slide: " + slideValues.current + "/" + slideValues.total
      : "Slide: --/--";
  }
  if (pBar) {
    pBar.style.width = progressValue + "%";
    pBar.style.backgroundColor = (progressValue === 100 && compColor) ? compColor : barColor;
  }
}

ensureUI();
updateUI();
setTimeout(updateUI, 150);
setTimeout(updateUI, 400);

if (!window.jbCourseHeaderUpdater) {
  window.jbCourseHeaderUpdater = setInterval(updateUI, 500);
}

JS Trigger: Scene Title

GitHub

Reads the current scene name and writes it into a Storyline variable for on-slide display. Works with the menu enabled or disabled, and on slides hidden from the menu.

v3.0.0Slide MasterInternal Data StoreMenu DOMHidden Slides

What It Does

Automatically detects which scene the learner is currently in and writes the scene name into a Storyline variable called SceneTitle. Reference it on any slide with %SceneTitle% and the current scene name appears without any hard-coding per slide. Without this script, you would need to manually type the scene name on every slide and update each one whenever you reorganize your course. Storyline does not provide a built-in variable for the scene name, so this script fills that gap. This version works whether the player menu is enabled or not, and it works on slides that have been hidden from the menu.

How It Works

The script uses three detection methods and tries them in order. Method 1 (Primary) reads the current slide ID from DS.presentation.attributes.slideMap.currentSlideId, extracts the scene ID portion, and matches it against DS.slideNumberManager.links to get the scene name. This works whether the player menu is enabled or not, and it works on slides that have been hidden from the menu. Method 2 (Fallback) reads the current slide title from the internal scene/slide model data and matches it against the child slide names in DS.slideNumberManager.links. Method 3 (Last Resort) searches the player menu DOM for scene headings and requires the menu to be enabled. If the menu has not loaded yet when Method 3 runs, it retries up to 20 times at 150 ms intervals.

Setup

  1. Open your Storyline project, go to View, then Slide Master, and select the parent Slide Master.
  2. Add a trigger: Execute JavaScript, paste the contents of the script, and set it to fire when the timeline starts. Storyline requires you to select an object on the Slide Master for the trigger to reference. Choose a persistent object that will always be present, such as the background image.
  3. On any slide where you want the scene name to appear, insert a text reference using %SceneTitle%.

Known Limitations and Risks

This script relies on parts of Storyline published output that Articulate does not officially document or support. Methods 1 and 2 read from a global DS object that Storyline exposes in its published HTML5 output. This is not a public API. Articulate has not documented it, does not support it, and could rename, restructure, or remove it in any future update without notice. If that happens, Methods 1 and 2 will stop working and the script will fall back to Method 3, which requires the course menu to be enabled. If the menu is disabled at that point, all three methods fail and SceneTitle will contain a diagnostic string. Because of this, retest the script after every Storyline update before shipping courses.

Scene ID Matching Caveat

In Method 1, scene IDs are matched with indexOf() > -1 rather than strict equality, because the link slideid field is formatted as "_player.sceneId" and the exact format is undocumented. If Storyline ever generates scene IDs where one is a substring of another (e.g. scene 6Jy7 existing alongside scene 6Jy77xK), the script could match the wrong scene. This has not been observed in practice, but it is a reason to avoid duplicate or near-duplicate slide names in the same course structure.

Hidden Slides

This script works on slides that have been hidden from the player menu. This is a key improvement over v2.0.0 and over using built-in Storyline variables. The built-in Menu.SlideTitle returns blank on slides hidden from the menu even when the menu is enabled, so any approach that depends on it will fail on hidden slides. Version 3.0.0 avoids this by reading the current slide ID directly from the internal data store, which tracks all slides regardless of menu visibility.

Menu Numbering

This script works whether player menu numbering is on or off. Slide matching uses an endsWith comparison rather than strict equality, so a numbered entry like "8.17. Decision 3" still matches the slide title "Decision 3". Scene names are stripped of any leading number prefix before being written to SceneTitle, so the variable always contains a clean name.

HTML Entity Decoding

Scene names from the internal data store may contain HTML entities such as & instead of &. The script decodes these automatically so SceneTitle displays clean text like "Curriculum & Lesson Plan Design" instead of "Curriculum & Lesson Plan Design".

Diagnostic Values

Value in SceneTitleMeaning
NO slide title availableThe script could not read a slide title from the data store or from a CurrentSlideTitle variable (Method 3 only)
NO menu rowsAll three methods failed. Data store methods found no match and the menu DOM is not available
NO slide match: [title]No menu entry matched the current slide title (Method 3 only)
NO row indexThe matched row was not found in the full row list
BLANK scene rowA scene heading was found but had no text
NO parent sceneThe slide appears before any scene heading in the menu (Method 3 only)

Changelog

VersionDateNotes
3.0.02026-04-09Rewrote primary detection to use the current slide ID from the internal data store. No longer requires a CurrentSlideTitle variable or a Menu.SlideTitle trigger. Works on slides hidden from the menu.
2.0.02026-04-08Added data store title matching so the script works without the player menu enabled. HTML entity decoding added.
1.0.12026-04-08Fixed slide matching to use endsWith so menu numbering prefixes do not break the lookup. Scene names stripped of number prefixes.
1.0.02026-04-06Initial release.
/**
 * ============================================================================
 * JavaScript Trigger Scene Title in Storyline
 * ============================================================================
 *
 * Purpose:     Determines which scene the current slide belongs to and
 *              writes the scene name into a Storyline variable called
 *              "SceneTitle". This lets authors display the current scene
 *              name on-slide (via a text reference to %SceneTitle%)
 *              without manually typing the scene name on every slide.
 *
 *              Storyline does not provide a built-in variable for the
 *              scene name, so this script fills that gap.
 *
 * Author:      Joseph Black
 * Date:        2026-04-09
 * Version:     3.0.0
 *
 * Software:    Articulate Storyline 360 x64 v3.114.36620.0
 *
 * Usage:       Add this script as an "Execute JavaScript" trigger on the
 *              parent Slide Master. Set it to fire when the timeline
 *              starts on this slide. Then reference the variable on any
 *              slide using %SceneTitle%.
 *
 *              No other triggers or variables are required. The script
 *              reads the current slide ID and scene name directly from
 *              Storyline's internal data store.
 *
 * Detection:   The script uses three methods and tries them in order:
 *
 *              Method 1 (Primary) - Slide ID Lookup:
 *              Reads the current slide ID from the internal data store
 *              (DS.presentation.attributes.slideMap.currentSlideId),
 *              extracts the scene ID from it, and matches it against
 *              DS.slideNumberManager.links to get the scene name. This
 *              works whether the menu is enabled or not and works on
 *              slides that have been hidden from the menu.
 *
 *              Method 2 (Fallback) - Title Matching via Data Store:
 *              If the slide ID lookup fails, reads the current slide
 *              title from the internal data store and matches it against
 *              the slide names in DS.slideNumberManager.links.
 *
 *              Method 3 (Last Resort) - Menu DOM:
 *              If both data store methods fail, searches the player menu
 *              DOM for scene headings. This requires the course menu to
 *              be enabled in the player.
 *
 * Notes:       - Works with or without the player menu enabled.
 *              - Works on slides that have been hidden from the menu.
 *              - Works whether player menu numbering is on or off.
 *              - No extra triggers or variables are needed beyond the
 *                single Execute JavaScript trigger.
 *              - HTML entities in scene names (e.g. &) are decoded
 *                automatically.
 *              - Diagnostic strings (prefixed "ERR", "NO", "BLANK") are
 *                written to SceneTitle when lookups fail, making it easy
 *                to troubleshoot during development.
 *
 * Changelog:   3.0.0 (2026-04-09) - Rewrote primary detection to use
 *                the current slide ID from the internal data store. The
 *                script no longer requires a CurrentSlideTitle variable
 *                or a Menu.SlideTitle trigger. Works on slides hidden
 *                from the menu. Reduced setup to a single trigger.
 *              2.0.0 (2026-04-08) - Added data store title matching so
 *                the script works without the player menu enabled.
 *              1.0.1 (2026-04-08) - Fixed slide matching to use endsWith
 *                instead of strict equality so menu numbering prefixes
 *                do not break the lookup. Added number prefix stripping
 *                on scene titles.
 *              1.0.0 (2026-04-06) - Initial release.
 * ============================================================================
 */

(function () {

  /* --- Storyline player API reference --- */
  var player = GetPlayer();

  /* --- Retry settings for waiting on data/DOM --- */
  var maxAttempts = 20;   // Maximum number of retries
  var retryDelay  = 150;  // Milliseconds between retries

  /**
   * normalizeText - Collapses all whitespace in a string down to single
   * spaces and trims the ends. Used to safely compare slide titles that
   * may contain extra whitespace or line breaks.
   */
  function normalizeText(value) {
    return (value || "").replace(/\s+/g, " ").trim();
  }

  /**
   * decodeHtmlEntities - Converts HTML entities (e.g. & < >)
   * back to their normal characters. Scene names from the internal data
   * store may contain HTML entities that need to be cleaned up before
   * displaying on-slide.
   */
  function decodeHtmlEntities(str) {
    var el = document.createElement("textarea");
    el.innerHTML = str;
    return el.value;
  }

  /**
   * cleanSceneName - Decodes HTML entities and strips any leading menu
   * numbering prefix (e.g. "2.  Module 1" becomes "Module 1"). This
   * ensures SceneTitle contains a clean name regardless of whether
   * menu numbering is on or off.
   */
  function cleanSceneName(raw) {
    var decoded = normalizeText(decodeHtmlEntities(raw || ""));
    return decoded.replace(/^\d+\.\s*/, "");
  }

  /* =======================================================================
   * METHOD 1 (Primary) - Slide ID Lookup
   *
   * Reads the current slide ID from the internal data store. The ID is
   * formatted as "sceneId.slideId". The script extracts the scene ID
   * portion and matches it against DS.slideNumberManager.links, which
   * contains scene objects with their display names.
   *
   * This works whether the menu is enabled or not, and it works on
   * slides that have been hidden from the menu.
   * ======================================================================= */

  /**
   * getCurrentSlideId - Safely reads the current slide ID from the
   * internal data store. Returns null if not available.
   */
  function getCurrentSlideId() {
    try {
      var slideMap = DS.presentation.attributes.slideMap;
      if (slideMap && slideMap.currentSlideId) {
        return slideMap.currentSlideId;
      }
    } catch (e) {}
    return null;
  }

  /**
   * getLinksData - Safely retrieves the links array from the internal
   * data store. Returns null if the data store is not available.
   */
  function getLinksData() {
    try {
      if (typeof DS !== "undefined" &&
          DS.slideNumberManager &&
          DS.slideNumberManager.links &&
          DS.slideNumberManager.links.length > 0) {
        return DS.slideNumberManager.links;
      }
    } catch (e) {}
    return null;
  }

  /**
   * findSceneBySlideId - Extracts the scene ID from the current slide
   * ID and matches it against the links data to find the scene name.
   * Returns the cleaned scene name, or null if no match is found.
   */
  function findSceneBySlideId() {
    var currentId = getCurrentSlideId();
    if (!currentId) return null;

    /* The slide ID is formatted as "sceneId.slideId" */
    var parts = currentId.split(".");
    if (parts.length < 2) return null;

    var sceneId = parts[0];
    var links = getLinksData();
    if (!links) return null;

    /* Match the scene ID against the links. The link slideid is
       formatted as "_player.sceneId" or similar, so we check if
       it contains the scene ID. */
    for (var i = 0; i < links.length; i++) {
      if (links[i].slideid && links[i].slideid.indexOf(sceneId) > -1) {
        var sceneName = cleanSceneName(links[i].displaytext);
        return sceneName || null;
      }
    }

    return null;
  }

  /* =======================================================================
   * METHOD 2 (Fallback) - Title Matching via Data Store
   *
   * If the slide ID lookup fails, this method reads the current slide
   * title from the scene/slide model data and matches it against the
   * child slides in DS.slideNumberManager.links.
   * ======================================================================= */

  /**
   * getCurrentSlideTitleFromDS - Reads the current slide title from the
   * internal data store by finding the slide model in the scenes
   * collection. Returns null if not available.
   */
  function getCurrentSlideTitleFromDS() {
    try {
      var currentId = getCurrentSlideId();
      if (!currentId) return null;

      var parts = currentId.split(".");
      if (parts.length < 2) return null;

      var sceneId = parts[0];
      var slideId = parts[1];

      var scenes = DS.presentation.attributes.scenes.models;
      for (var i = 0; i < scenes.length; i++) {
        if (scenes[i].id === sceneId) {
          var slides = scenes[i].attributes.slides;
          if (slides && slides.models) {
            var found = slides.models.find(function(s) {
              return s.id === slideId;
            });
            if (found && found.attributes && found.attributes.title) {
              return normalizeText(found.attributes.title);
            }
          }
        }
      }
    } catch (e) {}
    return null;
  }

  /**
   * findSceneBySlideTitle - Matches the slide title from the data store
   * against child slides in DS.slideNumberManager.links to find the
   * parent scene name. Returns the cleaned scene name, or null if no
   * match is found.
   */
  function findSceneBySlideTitle() {
    var slideTitle = getCurrentSlideTitleFromDS();
    if (!slideTitle) return null;

    var links = getLinksData();
    if (!links) return null;

    for (var i = 0; i < links.length; i++) {
      var children = links[i].links;
      if (!children) continue;

      for (var j = 0; j < children.length; j++) {
        var childTitle = normalizeText(decodeHtmlEntities(children[j].displaytext || ""));

        if (childTitle === slideTitle || childTitle.endsWith(slideTitle)) {
          var sceneName = cleanSceneName(links[i].displaytext);
          return sceneName || null;
        }
      }
    }

    return null;
  }

  /* =======================================================================
   * METHOD 3 (Last Resort) - Menu DOM
   *
   * Searches the player menu for scene headings marked with
   * data-is-scene="true". Requires the course menu to be enabled.
   * ======================================================================= */

  /**
   * isSceneRow - Returns true if a menu list-item represents a scene
   * heading rather than an individual slide. Storyline marks these
   * with a data-is-scene="true" attribute.
   */
  function isSceneRow(row) {
    return row && row.getAttribute("data-is-scene") === "true";
  }

  /**
   * getMenuRows - Returns an array of all menu list-item elements
   * (both scene headings and slide entries) from the player sidebar.
   */
  function getMenuRows() {
    return Array.from(
      document.querySelectorAll('.cs-listitem.listitem[data-slide-title]')
    );
  }

  /**
   * findSceneFromMenuDOM - Reads the slide title from the data store,
   * searches the player menu DOM for the matching slide, then walks
   * backward to find the scene heading. Returns the scene name or a
   * diagnostic string.
   */
  function findSceneFromMenuDOM() {
    /* Try getting the title from the data store */
    var currentSlideTitle = getCurrentSlideTitleFromDS() || "";

    /* Also try CurrentSlideTitle variable if data store failed */
    if (!currentSlideTitle) {
      try {
        currentSlideTitle = normalizeText(player.GetVar("CurrentSlideTitle"));
      } catch (e) {}
    }

    if (!currentSlideTitle) return "NO slide title available";

    var rows = getMenuRows();
    if (!rows.length) return "NO menu rows";

    /* Filter to slide-only rows and find the current slide.
       Uses endsWith so "8.17. Decision 3" matches "Decision 3"
       when menu numbering is enabled. */
    var slideRows = rows.filter(function (row) {
      return !isSceneRow(row);
    });

    var matchingSlideRow = slideRows.find(function (row) {
      return normalizeText(row.getAttribute("data-slide-title")).endsWith(currentSlideTitle);
    });

    if (!matchingSlideRow) return "NO slide match: " + currentSlideTitle;

    /* Walk backward from the matched slide to find its scene */
    var rowIndex = rows.indexOf(matchingSlideRow);
    if (rowIndex < 0) return "NO row index";

    for (var i = rowIndex - 1; i >= 0; i--) {
      var row = rows[i];
      if (isSceneRow(row)) {
        var sceneTitle = cleanSceneName(
          row.getAttribute("data-slide-title") || row.textContent
        );
        return sceneTitle || "BLANK scene row";
      }
    }

    return "NO parent scene";
  }

  /* =======================================================================
   * MAIN LOGIC
   *
   * Tries each detection method in order:
   *   1. Slide ID lookup via internal data store
   *   2. Title matching via internal data store
   *   3. Menu DOM search (last resort)
   * ======================================================================= */

  /**
   * updateSceneTitle - Main entry point. Attempts each method and writes
   * the result to the SceneTitle variable.
   */
  function updateSceneTitle(attempt) {
    attempt = attempt || 0;

    /* Method 1: Match by slide ID (works on hidden slides, no menu needed) */
    var sceneById = findSceneBySlideId();
    if (sceneById) {
      player.SetVar("SceneTitle", sceneById);
      return;
    }

    /* Method 2: Match by slide title from data store */
    var sceneByTitle = findSceneBySlideTitle();
    if (sceneByTitle) {
      player.SetVar("SceneTitle", sceneByTitle);
      return;
    }

    /* Method 3: Fall back to menu DOM */
    var menuRows = getMenuRows();

    if (!menuRows.length && attempt < maxAttempts) {
      /* Menu may not have rendered yet - retry */
      setTimeout(function () {
        updateSceneTitle(attempt + 1);
      }, retryDelay);
      return;
    }

    var sceneFromMenu = findSceneFromMenuDOM();
    player.SetVar("SceneTitle", sceneFromMenu);
  }

  /* --- Run immediately --- */
  updateSceneTitle();

})();

JS Trigger: Certificate Variables

GitHub

Populates two variables for a completion certificate: the learner name reformatted from the LMS, and the current date formatted as "Month D, YYYY".

v1.1.0SCORMLMS APIDateCompletion Slide

What It Does

Populates two Storyline variables for a course completion certificate. lmsName pulls the learner name from the LMS via the SCORM API and reformats it from "Last, First" to "First Last". completionDate builds the current date in "Month D, YYYY" format (e.g. "April 23, 2026") from the learner local system clock. Reference them on-slide with %lmsName% and %completionDate%. Storyline does not provide a built-in variable for the current date, and the built-in Project.LMSStudentName variable does not reformat the name, so this script fills both gaps.

How It Works

The name block calls lmsAPI.GetStudentName() to get the name from the LMS, splits the string on the comma, reassembles the parts as "First Last", and writes the result to lmsName. The date block creates a Date object from the local system clock, looks up the month name from an array (since JavaScript getMonth() returns 0-11), assembles the formatted string as "Month D, YYYY", and writes the result to completionDate. A single Execute JavaScript trigger populates both variables.

Setup

  1. Create two text variables in Storyline: lmsName and completionDate. Both should be blank by default.
  2. On the certificate slide, add a trigger: Execute JavaScript, set to fire when the timeline starts, and paste the contents of the script.
  3. Reference the variables on-slide with %lmsName% and %completionDate%, for example "Awarded to %lmsName% on %completionDate%".

Requirements and Considerations

The course must be hosted in an LMS that exposes the SCORM API for the name portion to work. lmsAPI is only available when the course runs from an LMS, so lmsName will not populate in Storyline preview or standalone published output. Most platforms return names as "Last, First" but some differ. If your LMS returns a different format, the parsing will need adjustment. The date portion has no external dependencies and works in any modern browser. completionDate reads from the learner local system clock, not a server, so an incorrect machine clock will produce an incorrect date on the certificate. For centrally managed workstations this is generally a non-issue. The script halts on the first error, so if the LMS name call fails the date block does not run. To guarantee the date always populates, swap the block order or wrap the name block in try/catch.

Changelog

VersionDateNotes
1.1.02026-04-24Added completionDate variable generation. Renamed from "Retrieve LMS Name" to "Certificate Variables" to reflect the broader purpose.
1.0.02026-04-06Initial release. Retrieves and reformats learner name from LMS as lmsName.
/**
 * ============================================================================
 * JavaScript Trigger Certificate Variables in Storyline
 * ============================================================================
 *
 * Purpose:     Populates two Storyline variables used on a course completion
 *              certificate:
 *
 *                1. lmsName        - The learner's name, retrieved from the
 *                                    LMS via the SCORM API and reformatted
 *                                    from "Last, First" to "First Last".
 *
 *                2. completionDate - The current date, formatted as
 *                                    "Month D, YYYY" (e.g. "April 23, 2026"),
 *                                    based on the learner's local system
 *                                    clock.
 *
 *              Together these allow the course to display a personalized
 *              completion certificate with the learner's name and the date
 *              they completed the course.
 *
 * Author:      Joseph Black
 * Date:        2026-04-24
 * Version:     1.1.0
 *
 * Software:    Articulate Storyline 360 x64 v3.115.36688.0
 *
 * Usage:       Add this script as a single "Execute JavaScript" trigger on
 *              the certificate slide. Set it to fire when the timeline
 *              starts. Two Storyline text variables must exist:
 *
 *                - lmsName
 *                - completionDate
 *
 *              Reference them on-slide with %lmsName% and %completionDate%.
 *
 * Notes:       - Requires the course to be hosted in an LMS that exposes
 *                the SCORM API (lmsAPI.GetStudentName). The name portion
 *                will not work in Storyline preview or standalone published
 *                output - the LMS API is only available when launched from
 *                an LMS.
 *              - Assumes the LMS returns the name in "Last, First" format
 *                (comma-separated). If the LMS uses a different format,
 *                the parsing logic will need adjustment.
 *              - The date is read from the learner's local system clock,
 *                not a server. If the machine clock is incorrect, the
 *                certificate will reflect that. For centrally managed
 *                workstations this is generally a non-issue.
 *              - If lmsAPI.GetStudentName() throws (e.g. when launched
 *                outside an LMS), the date portion will not run because
 *                the script halts at the error. To guarantee the date
 *                always populates, move the date block above the name
 *                block or wrap the name block in try/catch.
 *
 * Changelog:   1.1.0 (2026-04-24) - Added completionDate variable
 *                generation. Renamed script from "Retrieve LMS Name" to
 *                "Certificate Variables" to reflect the broader purpose.
 *              1.0.0 (2026-04-06) - Initial release: retrieves and
 *                reformats learner name from LMS as lmsName.
 * ============================================================================
 */

/* Get a reference to the Storyline player API */
let player = GetPlayer();

/* ---------- Learner Name ---------- */

/* Retrieve the learner's full name from the LMS via the SCORM API.
   Most LMS platforms return this in "Last, First" format. */
let myName = lmsAPI.GetStudentName();

/* Split the name on the comma into an array: ["Last", " First"] */
let array = myName.split(',');

/* Reassemble as "First Last" with a space between.
   array[1] is the first name (may have a leading space from the split).
   If the LMS leaves leading whitespace, add .trim() calls:
   let newName = array[1].trim() + ' ' + array[0].trim(); */
let newName = array[1] + ' ' + array[0];

/* Write the reformatted name into the Storyline variable */
player.SetVar("lmsName", newName);

/* ---------- Completion Date ---------- */

/* Create a Date object from the learner's local system clock.
   This captures the exact moment the certificate slide loads. */
let today = new Date();

/* Month names array - JavaScript's Date.getMonth() returns 0-11,
   so this lookup converts the zero-based index to a display name. */
let months = ["January", "February", "March", "April", "May", "June",
              "July", "August", "September", "October", "November", "December"];

/* Assemble the final display string as "Month D, YYYY".
   To use a different format, modify this assembly line. Common variants:
     MM/DD/YYYY:  (today.getMonth() + 1) + "/" + today.getDate() + "/" + today.getFullYear()
     DD Month YYYY:  today.getDate() + " " + months[today.getMonth()] + " " + today.getFullYear() */
let formatted = months[today.getMonth()] + " " + today.getDate() + ", " + today.getFullYear();

/* Write the formatted date into the Storyline variable */
player.SetVar("completionDate", formatted);