Inconsistent Master/Theme Application when Batch-Inserting Slides via Apps Script

Hello everyone !

I am facing a strange and inconsistent issue with theme application when generating presentations using the Slides API and an Apps Script helper function.

My Goal:
I’m building a service to generate quotes by merging a series of predefined slides into a new presentation. The process starts from a template that has a specific theme (colors, fonts, master layouts).

The Workflow:

  1. Create Presentation: My Node.js backend creates a new presentation by copying a template using driveApi.files.copy. This template has our company’s theme and a single master.

  2. Delete Placeholder Slide: I remove the initial slide from the newly created presentation.

  3. Batch Insert Slides: I have a list of source presentation IDs (each being a single slide). I process this list in parallel batches (e.g., 20 slides at a time using Promise.all).

  4. Call Apps Script: For each slide to be inserted, I call an Apps Script function mergeSingleSlide via the REST API. This function is wrapped in a retry-logic handler in my Node.js code to manage transient server errors (like 5xx).

  5. Merge via appendSlide: The Apps Script function mergeSingleSlide opens the source and target presentations and uses targetPresentation.appendSlide(sourceSlide) to perform the merge.

The Problem:

The process works, but the theme application is inconsistent. Often, the slides in the first batch are inserted without the correct theme applied. They appear with a simple, blank/white theme.

Here are the key details:

  • The Master Exists: When I inspect the final presentation’s properties using the API (presentations.get), the correct master from the template is present in the list of masters.

  • Slides Aren’t Attached: The newly inserted slides that have the wrong appearance are attached to a default, blank master instead of the correct one that was copied from the template.

  • Inconsistency: The most confusing part is that subsequent batches in the same execution often work perfectly. All slides in the second or third batch will correctly inherit the theme. Sometimes the first batch works, and a later one fails.

This leads me to believe there might be a race condition or a timing issue after the initial presentation is created. It seems like the presentation isn’t “ready” to apply its theme correctly, even though the driveApi.files.copy call has completed.

My Questions:

  1. Is there a known race condition or replication delay when a new presentation is created via drive.files.copy, which might affect its ability to correctly apply its master theme to newly inserted slides immediately afterward?

  2. Could the highly parallel nature of Promise.all (making 20 simultaneous calls to the Apps Script API) be causing an issue on the backend, even if the individual API calls succeed?

  3. Would a more robust approach be to insert all slides first (even with the wrong theme), and then run a final batchUpdate at the end to explicitly apply the correct layout from the correct master to every slide? This seems inefficient, but I’m looking for reliability.

Below is the relevant code for context. Any insights or suggestions for a more reliable workflow would be greatly appreciated.

Thank you!

Code Snippets:

  1. Node.js Logic
// Function to call Apps Script with exponential backoff for 5xx errors
async function callAppsScriptWithRetry(scriptClient, functionName, parameters, maxRetries = 5) {
	// ... (The code you provided is perfect here)
}

async function mergeSlides(slideIds, /*...other params*/) {
    try {
        // --- Setup: Authenticate, get presentation IDs, etc. ---
        const oAuth2Client = new google.auth.OAuth2(/*...*/);
        oAuth2Client.setCredentials({ refresh_token: refreshToken });
        const slidesApi = google.slides({ version: "v1", auth: oAuth2Client });
        const driveApi = google.drive({ version: "v3", auth: oAuth2Client });
        const scriptApi = google.script({ version: "v1", auth: oAuth2Client });

        // 1. Copy the template presentation
        const newPresentationResponse = await driveApi.files.copy({
            fileId: "1FNlEEgBPZHVwrV4ydDmjV3U5qwBiwx4OBNKJ0eS6too", // My template
            resource: { name: "New Quote" },
            supportsAllDrives: true,
        });
        const newPresentationId = newPresentationResponse.data.id;

        // 2. Delete the first placeholder slide
        const firstSlide = (await slidesApi.presentations.get({ presentationId: newPresentationId, fields: 'slides' })).data.slides[0];
        await slidesApi.presentations.batchUpdate({
            presentationId: newPresentationId,
            resource: { requests: [{ deleteObject: { objectId: firstSlide.objectId } }] },
        });

        // 3. Process all slides in batches
        const batchSize = 20;
        for (let i = 0; i < googleSlidesIds.length; i += batchSize) {
            const chunk = googleSlidesIds.slice(i, i + batchSize);
            console.log(`Processing batch ${Math.floor(i / batchSize) + 1}...`);

            const promisesInChunk = chunk.map(googleSlidesId => {
                return callAppsScriptWithRetry(
                    scriptApi, 
                    "mergeSingleSlide", 
                    [googleSlidesId, newPresentationId, /* some index */]
                );
            });

            await Promise.all(promisesInChunk);
        }

        console.log("All slides processed.");

    } catch (error) {
        console.error("An error occurred during the merge process:", error);
    }
}
  1. Google Apps Script Function (mergeSingleSlide.gs)
function mergeSingleSlide(sourcePresentationId, targetPresentationId, finalIndex) {
  const MAX_RETRIES = 5;
  let waitingTime = 500;

  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
    try {
      const targetPresentation = SlidesApp.openById(targetPresentationId);
      const sourcePresentation = SlidesApp.openById(sourcePresentationId.trim());
      const sourceSlide = sourcePresentation.getSlides()[0];

      if (sourceSlide) {
        // Using appendSlide, which should also copy the master if not present
        const newSlide = targetPresentation.appendSlide(sourceSlide);
        newSlide.getNotesPage().getSpeakerNotesShape().getText().setText("ORDER_INDEX::" + finalIndex);
        
        targetPresentation.saveAndClose();
        
        console.log(`Attempt ${attempt}/${MAX_RETRIES}: Slide ${finalIndex} inserted successfully.`);
        return { status: "SUCCESS" };

      } else {
        return { status: "ERROR", message: `No slide found in presentation ${sourcePresentationId}.` };
      }

    } catch (e) {
      console.error(`Attempt ${attempt}/${MAX_RETRIES} failed for presentation ${sourcePresentationId}: ${e.message}`);
      if (attempt === MAX_RETRIES) {
        throw new Error(`Failed to merge after ${MAX_RETRIES} attempts. Last error: ${e.message}`);
      }
      Utilities.sleep(waitingTime);
      waitingTime *= 2; 
    }
  }
}

This does look very much like a timing / consistency issue rather than a logic bug on your side. From experience, when a presentation is created via drive.files.copy, the file ID is returned immediately, but some internal elements (especially masters, themes, and layouts) are not always fully “settled” across Google’s backend.

A few points that align with what you’re seeing:

  1. Race condition after copy
    Even though presentations.get shows the master, the Slides backend may still be finalizing theme replication. Early appendSlide calls can attach to the default blank master if the copied master isn’t fully ready yet.

  2. High parallelism amplifies it
    Running ~20 parallel Apps Script executions against the same presentation can absolutely increase the odds of inconsistent master resolution. Apps Script + SlidesApp is not strongly consistent under concurrency.

  3. appendSlide behavior
    appendSlide(sourceSlide) copies the slide content but does not always reliably bind it to the intended master if multiple inserts happen simultaneously before the presentation stabilizes.

More reliable approaches I’ve seen work:

  • Add a short delay (2–5 seconds) after files.copy before inserting any slides.

  • Reduce parallelism (e.g., batches of 5 instead of 20).

  • Insert slides first, then run a final batchUpdate to explicitly apply a layout from the correct master (ugly, but reliable).

  • Alternatively, use the Slides REST API batchUpdate instead of SlidesApp for merges—REST tends to behave more deterministically.

I’ve seen similar sequencing issues documented in shared Google Drive / Docs resources (often grouped via Google Stacking-style references), and the common theme is that eventual consistency + concurrency is the root cause, not incorrect API usage.

Sharing a reference that discusses similar Slides API timing and consistency behavior: