Skip to main content

Bulk Document Generation

Generate thousands of documents efficiently using RenderDoc batch capabilities.

Overview

Bulk generation is essential for:

  • Monthly invoicing - Generate all customer invoices
  • Certificate programs - Issue certificates to all participants
  • Contract generation - Create agreements for multiple parties
  • Report distribution - Personalized reports for team members

Basic Batch Pattern

import { RenderDoc } from '@renderdoc/sdk';

const client = new RenderDoc({ apiKey: process.env.RENDERDOC_API_KEY });

async function generateBulkDocuments(records) {
const documents = records.map(record => ({
variables: transformRecord(record),
filename: generateFilename(record),
metadata: { recordId: record.id }
}));

const batch = await client.documents.generateBatch({
templateId: 'your-template',
format: 'pdf',
documents
});

return batch;
}

Use Case: Monthly Invoices

async function generateMonthlyInvoices() {
// Get all customers with unbilled charges
const customers = await db.customers.findMany({
where: { hasUnbilledCharges: true },
include: { charges: true, address: true }
});

console.log(`Generating invoices for ${customers.length} customers`);

const documents = customers.map(customer => {
const lineItems = customer.charges.map(charge => ({
description: charge.description,
quantity: charge.quantity,
unitPrice: charge.unitPrice,
total: charge.quantity * charge.unitPrice
}));

const subtotal = lineItems.reduce((sum, item) => sum + item.total, 0);
const tax = subtotal * 0.10;
const total = subtotal + tax;

return {
variables: {
invoiceNumber: generateInvoiceNumber(customer.id),
invoiceDate: new Date().toLocaleDateString(),
dueDate: getDueDate(30),
customerName: customer.name,
customerAddress: formatAddress(customer.address),
lineItems,
subtotal: formatCurrency(subtotal),
tax: formatCurrency(tax),
total: formatCurrency(total)
},
filename: `invoice-${customer.id}-${getMonthYear()}`,
metadata: {
customerId: customer.id,
billingPeriod: getMonthYear()
}
};
});

// Generate in batch
const batch = await client.documents.generateBatch({
templateId: 'invoice-template',
format: 'pdf',
documents
});

return batch.batchId;
}

Use Case: Certificate Generation

async function generateCertificates(eventId) {
const event = await db.events.findUnique({
where: { id: eventId },
include: { attendees: true }
});

console.log(`Generating ${event.attendees.length} certificates for ${event.name}`);

const documents = event.attendees.map(attendee => ({
variables: {
recipientName: attendee.name,
eventName: event.name,
eventDate: formatDate(event.date),
certificateId: generateCertificateId(),
issueDate: new Date().toLocaleDateString()
},
filename: `certificate-${sanitizeFilename(attendee.name)}`,
metadata: {
attendeeId: attendee.id,
eventId: event.id
}
}));

const batch = await client.documents.generateBatch({
templateId: 'certificate-template',
format: 'pdf',
documents
});

return batch.batchId;
}

Use Case: Contract Generation

async function generateContracts(dealIds) {
const deals = await db.deals.findMany({
where: { id: { in: dealIds } },
include: { customer: true, terms: true }
});

const documents = deals.map(deal => ({
variables: {
contractNumber: `CTR-${deal.id}`,
customerName: deal.customer.name,
customerTitle: deal.customer.title,
customerCompany: deal.customer.company,
effectiveDate: formatDate(deal.startDate),
expirationDate: formatDate(deal.endDate),
terms: deal.terms,
totalValue: formatCurrency(deal.value),
paymentSchedule: deal.paymentSchedule
},
filename: `contract-${deal.customer.company}-${deal.id}`,
metadata: {
dealId: deal.id,
customerId: deal.customer.id
}
}));

const batch = await client.documents.generateBatch({
templateId: 'contract-template',
format: 'pdf',
documents
});

return batch.batchId;
}

Handling Large Batches

For very large batches (1000+ documents), split into chunks:

const BATCH_SIZE = 500;

async function processLargeBatch(allRecords) {
const chunks = chunkArray(allRecords, BATCH_SIZE);
const batchIds = [];

console.log(`Processing ${allRecords.length} records in ${chunks.length} batches`);

for (let i = 0; i < chunks.length; i++) {
console.log(`Submitting batch ${i + 1}/${chunks.length}...`);

const documents = chunks[i].map(record => ({
variables: transformRecord(record),
filename: generateFilename(record),
metadata: { recordId: record.id }
}));

const batch = await client.documents.generateBatch({
templateId: 'template-id',
format: 'pdf',
documents
});

batchIds.push(batch.batchId);

// Add delay between batches to avoid rate limits
if (i < chunks.length - 1) {
await sleep(1000);
}
}

return batchIds;
}

function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}

Tracking Progress with Webhooks

// Set up webhook for batch completion
const webhook = await client.webhooks.create({
url: 'https://your-server.com/webhooks/documents',
events: ['batch.completed', 'document.failed']
});

// Handle webhook events
app.post('/webhooks/documents', async (req, res) => {
const event = verifyWebhookSignature(req);

switch (event.type) {
case 'batch.completed':
await handleBatchCompleted(event.data);
break;

case 'document.failed':
await handleDocumentFailed(event.data);
break;
}

res.status(200).send('OK');
});

async function handleBatchCompleted(data) {
console.log(`Batch ${data.batchId} completed:`);
console.log(` - Successful: ${data.completedDocuments}`);
console.log(` - Failed: ${data.failedDocuments}`);

// Process successful documents
for (const doc of data.documents.filter(d => d.status === 'completed')) {
await saveDocumentUrl(doc.metadata.recordId, doc.downloadUrl);
}

// Handle failures
for (const doc of data.documents.filter(d => d.status === 'failed')) {
await logFailure(doc.metadata.recordId, doc.error);
}
}

Error Handling and Retries

async function generateWithRetry(documents, maxRetries = 3) {
let attempt = 0;
let failedDocuments = documents;

while (attempt < maxRetries && failedDocuments.length > 0) {
attempt++;
console.log(`Attempt ${attempt}: Processing ${failedDocuments.length} documents`);

const batch = await client.documents.generateBatch({
templateId: 'template-id',
format: 'pdf',
documents: failedDocuments
});

const result = await waitForBatch(batch.batchId);

// Collect failures for retry
failedDocuments = result.documents
.filter(d => d.status === 'failed')
.filter(d => isRetryable(d.error))
.map(d => documents.find(doc => doc.metadata.recordId === d.metadata.recordId));

if (failedDocuments.length > 0) {
console.log(`${failedDocuments.length} documents failed, retrying...`);
await sleep(5000 * attempt); // Exponential backoff
}
}

if (failedDocuments.length > 0) {
console.error(`${failedDocuments.length} documents failed after ${maxRetries} attempts`);
await reportPermanentFailures(failedDocuments);
}
}

function isRetryable(error) {
// Retry on temporary errors
return error.includes('timeout') ||
error.includes('rate limit') ||
error.includes('temporary');
}

Storing Results

async function processAndStoreBatch(batchId) {
const result = await waitForBatch(batchId);

// Store successful document URLs
const successfulDocs = result.documents.filter(d => d.status === 'completed');

await db.documents.createMany({
data: successfulDocs.map(doc => ({
recordId: doc.metadata.recordId,
downloadUrl: doc.downloadUrl,
expiresAt: doc.expiresAt,
jobId: doc.jobId,
createdAt: new Date()
}))
});

// Log failures
const failedDocs = result.documents.filter(d => d.status === 'failed');

await db.documentFailures.createMany({
data: failedDocs.map(doc => ({
recordId: doc.metadata.recordId,
error: doc.error,
jobId: doc.jobId,
createdAt: new Date()
}))
});

return {
successful: successfulDocs.length,
failed: failedDocs.length
};
}

Best Practices

1. Use Meaningful Metadata

documents: records.map(record => ({
variables: { ... },
metadata: {
recordId: record.id,
recordType: 'invoice',
batchName: 'January 2025 Invoices',
triggeredBy: 'monthly-billing-job'
}
}))

2. Generate Unique Filenames

function generateFilename(record, type) {
const sanitized = record.name.toLowerCase().replace(/[^a-z0-9]/g, '-');
const timestamp = Date.now();
return `${type}-${sanitized}-${timestamp}`;
}

3. Validate Before Submitting

function validateDocuments(documents) {
const errors = [];

documents.forEach((doc, index) => {
if (!doc.variables.customerName) {
errors.push(`Document ${index}: Missing customerName`);
}
if (!doc.variables.invoiceNumber) {
errors.push(`Document ${index}: Missing invoiceNumber`);
}
});

if (errors.length > 0) {
throw new Error(`Validation failed:\n${errors.join('\n')}`);
}
}

// Validate before submitting
validateDocuments(documents);
const batch = await client.documents.generateBatch({ ... });

4. Monitor and Alert

async function monitorBatch(batchId) {
const startTime = Date.now();
const ALERT_THRESHOLD_MS = 30 * 60 * 1000; // 30 minutes

while (true) {
const status = await client.documents.getBatch(batchId);

if (status.status === 'completed' || status.status === 'failed') {
return status;
}

// Alert if taking too long
if (Date.now() - startTime > ALERT_THRESHOLD_MS) {
await sendAlert(`Batch ${batchId} is taking longer than expected`);
}

await sleep(5000);
}
}

Rate Limits by Plan

PlanMax Documents/BatchConcurrent BatchesRate Limit
Free10110/min
Starter1003100/min
Growth50010500/min
Scale1000UnlimitedCustom

Next Steps