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:
- 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. - Delete Placeholder Slide: I remove the initial slide from the newly created presentation.
- 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). - Call Apps Script: For each slide to be inserted, I call an Apps Script function
mergeSingleSlidevia the REST API. This function is wrapped in a retry-logic handler in my Node.js code to manage transient server errors (like 5xx). - Merge via appendSlide: The Apps Script function
mergeSingleSlideopens the source and target presentations and usestargetPresentation.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.copycall has completed.
My Questions:
- 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? - 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?
- 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 :
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);
}
}
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;
}
}
}