Skip to Content
DevelopersConceptsTemplate versioning

Template versioning

Channel templates — WhatsApp HSMs, SMS templates with DLT registration, approved email designs — have an external lifecycle (Meta / TRAI / SMTP-provider approval) that doesn’t align with a typical software-release cadence. Active Reach handles this by versioning every template, keeping in-flight journeys on the version they entered on, and rolling new versions out only to new entrants.

Why versioning

Without versioning, editing a template breaks two things:

  1. In-flight journeys: a contact mid-sequence may have a wait-then-send pattern; the send arrives with the new template, which doesn’t match the trigger-time intent.
  2. External approval state: editing a WhatsApp HSM resets its Meta approval status — the template is unsendable until re-approval, which can take hours.

Versioning gives every edit a stable identifier. In-flight journeys hold the version they entered with. New entrants get the latest approved version. Approvals are per-version, not per-template.

Template lifecycle

Draft → Submitted → Approved → Active → Superseded Rejected
StateBehaviour
DraftEditable; not sendable; not submitted upstream
SubmittedSent to upstream (Meta WABA, TRAI DLT, etc.); awaiting verdict
ApprovedUpstream said yes; sendable; will be assigned to new journey entries
ActiveA specific version that’s the current default for new entries
SupersededAn older approved version; still sendable for in-flight journeys; not picked for new entries
RejectedUpstream rejected; not sendable; the operator must edit and resubmit

The Active version is implicit: the most-recently-approved version of the template, unless an operator pins an older version.

What gets versioned

Every property of the template that affects its rendering or upstream approval:

  • Body text (with variable placeholders)
  • Header (text / image / video / document)
  • Footer text
  • Button definitions (URL / phone / quick-reply)
  • Category (marketing / utility / authentication for WhatsApp)
  • Language

Variables themselves are NOT versioned — they’re resolved from contact context at send time. Updating the variable schema (adding customer_first_name, deprecating customer_full_name) is a separate migration tracked at the template variable registry  level.

In-flight journey pinning

When a contact enters a journey, the journey snapshot records the template version IDs for every send node it’ll hit. Subsequent template edits don’t touch this snapshot — the contact will see the templates the operator approved at the time the contact entered.

The exception: forced rollout can override pinning. An operator can mark a new version as force_rollout: true, which makes in-flight journeys switch to the new version on their next send. This is destructive — used only when the previous version is genuinely broken (e.g. legal-mandated text correction).

Cross-channel templates

The unified channel template lifecycle covers templates that exist across multiple channels. For example, a “welcome” template might have:

  • A WhatsApp HSM (Meta-approved)
  • An SMS variant (DLT-registered)
  • An email design (HTML + DKIM-signed sender)

These are three separate version trees, but the unified template ties them together so an operator can edit “the welcome template” and see all three channels in one view. Approval is independent per channel — the WhatsApp HSM can be approved while SMS waits on DLT.

Catalog architecture

Templates that drive automations live on the control plane alongside the rest of the catalog:

  • component_templates — reusable journey components (use the graph schema)
  • playbook_templates — full playbook definitions
  • ad_campaign_templates — paid-domain campaign skeletons
  • channel_templates — per-channel message templates (WhatsApp HSM, SMS DLT, email designs)
  • unified_template_catalog — index over all of the above

sync_catalog.py upserts every template into unified_template_catalog within the same transaction as the source write (via SQLAlchemy after_insert / after_update listeners — no cross-plane HTTP). The admin catalog drawer reads metadata from unified_template_catalog and fetches full source data via /api/v1/admin/catalog/entries/{id}/source-detail.

When a new template type is added, it must (a) be synced into unified_template_catalog and (b) have a visual renderer in admin-template-preview.tsx.

Operational instances of these templates — journeys, journey_instances, playbook_execution_instances, ad_campaign_instances, ad_campaign_stage_state, ad_campaign_audience_flows — live on the cell plane.

API shape

Templates are exposed under /api/v1/templates/:

EndpointPurpose
GET /templates/{id}Returns the template with its current Active version and all approved versions
GET /templates/{id}/versionsLists every version with its state
POST /templates/{id}/versionsCreates a new draft version (does not submit)
POST /templates/{id}/versions/{version_id}/submitSubmits to upstream for approval
POST /templates/{id}/versions/{version_id}/activateMarks an approved version as Active

Send-time version resolution

When a journey send fires, the engine resolves the template version in this order:

  1. The version_id pinned in the journey snapshot for this contact (if pinned)
  2. The version_id pinned by force_rollout (if any)
  3. The current Active version of the template

Resolution emits a record so you can later audit which version any historical send used.

Failure modes the lifecycle handles

ScenarioBehaviour
Upstream approval times outTemplate stays in Submitted; alerts the operator after N hours
Upstream rejects the versionState → Rejected; send nodes pointing at this template fall back to the previous Active version
Operator deletes a templateRefused if any in-flight journey is pinned to it; otherwise marked Archived (not actually deleted)
Variable schema change breaks an in-flight pinDetected at version-pin time; in-flight contacts are migrated to the latest version with a per-contact note in the timeline

What’s next