Jobs that reference a template by mutable name always resolve to the current version. When a template is updated mid-campaign, queued jobs that have not yet been processed will render the new version instead of the version that was live when the campaign was dispatched. Recipients in the same send batch receive different content depending on when their job was processed. ISO-25010:2011 maintainability.modifiability requires that changes to one component do not silently alter the behavior of unrelated queued work.
Low because mid-campaign template mutations affect content consistency rather than causing data loss, security exposure, or system failure.
Pin a templateVersion identifier on every job at enqueue time so the worker loads the exact version that was current when the campaign was dispatched:
await emailQueue.add('send', {
to: recipient.email,
templateId: 'welcome',
templateVersion: 'v2.1.0', // Pinned at dispatch time
mergeFields: { firstName: recipient.firstName }
})
In the worker, query db.emailTemplate.findFirstOrThrow({ where: { slug: templateId, version: templateVersion } }) so a template update never silently alters in-flight jobs.
ID: sending-pipeline-infrastructure.template-engine.template-versioning
Severity: low
What to look for: Check how email templates are versioned. Look for templates stored in files with no version tracking, or a CMS/database table for templates that has no version column. Versioning matters because changing a live template mid-campaign can alter emails already queued and mid-flight. Check whether queued job payloads reference a template by a stable version identifier, or by a mutable name that always resolves to the latest version.
Pass criteria: Templates have a version identifier. Queued jobs reference a specific template version, not a mutable pointer. Count all job enqueue call sites — 100% must include a templateVersion field or equivalent pinned reference. Deploying a new template version does not alter jobs already in the queue. At least 2 previous versions can be retrieved for comparison.
Fail criteria: Jobs reference templates by name only (resolving to the current version). Updating a template mid-campaign changes the content of emails already queued. No history of template changes is available.
Skip (N/A) when: The application sends only immediate transactional emails with no queued batches — confirmed by the absence of batch or campaign send paths.
Detail on fail: "Queued jobs reference template by name (e.g., 'welcome') — changing the template mid-campaign alters queued sends" or "Templates stored in files with no version identifier — no history of what changed between versions"
Remediation: Store template version IDs on queued jobs:
// When enqueuing a job, include the explicit template version
await emailQueue.add('send', {
to: recipient.email,
templateId: 'welcome',
templateVersion: 'v2.1.0', // Pinned at queue time
mergeFields: { firstName: recipient.firstName }
})
// In the worker, load the specific version
async function processEmailJob(job: Job<EmailJobData>) {
const template = await db.emailTemplate.findFirstOrThrow({
where: {
slug: job.data.templateId,
version: job.data.templateVersion
}
})
const html = renderTemplate(template.htmlSource, job.data.mergeFields)
// ...
}