When sending personalized emails from Salesforce Marketing Cloud via Journey Builder, especially those triggered by Salesforce CRM events, you may need to view the exact rendered email that was sent to a specific subscriber directly from within Sales Cloud. Standard Marketing Cloud tracking does not retain a copy of each individually rendered email. The built-in View As Web Page link provides a re-rendering but it breaks if the source Data Extension is later cleared or deleted.
This article explains how to capture the fully rendered email HTML at send time, store it in a Data Extension, and present it to Sales Cloud users on the Contact or Lead record page via a Lightning Web Component with a modal preview.
Option A: Capture HTML at Send Time via AMPscript
Step 1: Understand open tracking pixel isolation
%%[
IF _messagecontext == "SEND" THEN
SET @VawpUrl = view_email_url
SET @subscriberkey = _subscriberkey
SET @emailname = emailname_
SET @emailHTML = HTTPGet(@VawpUrl)
SET @archiveKey = Concat(
@subscriberkey, "_", jobid)
]%%
<!-- Open tracking pixel: ONLY rendered at
send time, excluded from VAWP copy -->
<custom name="opencounter" type="tracking"/>
%%[
ENDIF
]%%
Step 2: Understanding how this works
Step 3: Recommendation for high-value emails
Given the 2× supermessage cost and added send latency, it is not practical or necessary to archive the HTML of every marketing email. Instead, apply this pattern selectively to emails where CRM users, such as Sales or Service agents genuinely need to see the exact rendered content. Good candidates include:
Option B: Capture HTML Post-Send via SSJS Script Activity
Step 1: Batch HTML Retrieval from Send LOG Using SSJS
Note: Rows.Retrieve() has a hard limit of 2,500 rows per call. If you have more than 2,500 unarchived Send Log rows, a single retrieval will silently return only the first 2,500. To handle this, wrap the retrieval in a loop that continues fetching until fewer than 2,500 rows are returned
<script runat="server">
Platform.Load("core", "1.1.1");
var sendLogDE = DataExtension.Init("SendLog");
var archiveDE = DataExtension.Init(
"Aggregated_marketing_engagement");
var filter = {
Property: "Archived",
SimpleOperator: "equals",
Value: "false"
};
/* Loop to handle the 2,500-row retrieval
limit on Rows.Retrieve() */
var batchSize = 2500;
var rows;
do {
rows = sendLogDE.Rows.Retrieve(filter);
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
var vawpUrl = row["VAWP_URL"];
var subKey = row["SubscriberKey"];
var jobId = row["JobID"];
if (vawpUrl && vawpUrl.length > 0) {
try {
var resp = HTTP.Get(vawpUrl);
var html = resp.Content;
archiveDE.Rows.Add({
ArchiveKey:
subKey + "_" + jobId,
SubscriberKey: subKey,
JobID: jobId,
EmailName:
row["EmailName"],
EmailHTML: html,
SentDate:
row["SentDate"]
});
/* Flag as archived */
sendLogDE.Rows.Update(
{ Archived: "true" },
["SubID", "JobID"],
[row["SubID"], jobId]
);
} catch (e) {
/* Log error, continue */
}
}
}
/* Continue if we hit the 2,500 limit,
meaning more rows may exist */
} while (rows.length >= batchSize);
</script>
Step 2: Engagement JSON Retrieval via Code Resource
The engagement Code Resource returns a lightweight JSON payload for a given Subscriber Key, querying the Aggregated_marketing_engagement DE. It includes the archiveKey per email so the LWC can request the HTML separately, but does not include the HTML itself
%%[
VAR @id, @rows, @rowCount, @i, @row
VAR @emailName, @opened, @clicked
VAR @bounced, @sentDate, @archiveKey
VAR @journeyName
SET @id = RequestParameter("id")
SET @rows = LookupOrderedRows(
"Aggregated_marketing_engagement",
50,
"eventdate DESC",
"SubscriberKey", @id)
SET @rowCount = RowCount(@rows)
Output(Concat("{", Char(10), ' "Emails": ['))
IF @rowCount > 0 THEN
FOR @i = 1 TO @rowCount DO
SET @row = Row(@rows, @i)
SET @emailName = Field(@row, "emailName")
SET @opened = Field(@row, "opened")
SET @clicked = Field(@row, "clicked")
SET @bounced = Field(@row, "bounced")
SET @sentDate = Field(@row, "eventdate")
SET @archiveKey = Field(@row, "archiveKey")
SET @journeyName = Field(@row, "JourneyName")
Output(Concat(Char(10), " {"))
Output(Concat(
' "name": "', @emailName, '",'))
Output(Concat(
' "archiveKey": "',
@archiveKey, '",'))
Output(Concat(
' "opened": ',
IIF(@opened == "True",
"true", "false"), ','))
Output(Concat(
' "clicked": ',
IIF(@clicked == "True",
"true", "false"), ','))
Output(Concat(
' "bounced": ',
IIF(EMPTY(@bounced) OR @bounced != "True",
"false", "true"), ','))
Output(Concat(
' "sent": "', @sentDate, '",'))
Output(Concat(
' "JourneyName": "',
@journeyName, '"'))
Output(Concat(" }",
IIF(@i < @rowCount, ",", "")))
NEXT @i
ENDIF
Output(Concat(Char(10), " ]", Char(10), "}"))
]%%
{
"Emails": [
{
"name": "Email 3 - Weather forecast",
"archiveKey": "00Qd2000007..._166768",
"opened": false,
"clicked": false,
"bounced": false,
"sent": "2026-04-07T00:00:00",
"JourneyName": ""
}
]
}
Step 3: HTML Rendering via Cloud Page Endpoint
%%[
VAR @key, @html
SET @key = RequestParameter("key")
SET @html = Lookup(
"Aggregated_marketing_engagement",
"emailHtml",
"archiveKey", @key)
IF NOT EMPTY(@html) THEN
Output(@html)
ELSE
Output("<html><body><p>Email content not found. The archive may have expired.</p></body></html>")
ENDIF
]%%
Step 4: Lightning Web Component with Email Preview
<template>
<template lwc:if={hasEmails}>
<lightning-card
title="Email Engagement"
icon-name="standard:email">
<lightning-datatable
key-field="id"
data={emails}
columns={columns}
onrowaction={handleRowAction}
hide-checkbox-column>
</lightning-datatable>
</lightning-card>
</template>
<!-- Email preview modal -->
<template lwc:if={showModal}>
<section role="dialog" aria-modal="true"
class="slds-modal slds-fade-in-open">
<div class="slds-modal__container"
style="max-width:960px;">
<header
class="slds-modal__header">
<button
class="slds-button
slds-button_icon
slds-modal__close
slds-button_icon-inverse"
onclick={closeModal}>
<lightning-icon
icon-name="utility:close"
size="small"
variant="inverse">
</lightning-icon>
</button>
<h2 class="slds-modal__title
slds-hyphenate">
{previewTitle}</h2>
</header>
<div class="slds-modal__content
slds-p-around_none"
style="height:70vh;">
<iframe
src={previewUrl}
title={previewTitle}
style="width:100%;
height:100%;
border:none;">
</iframe>
</div>
</div>
</section>
<div class="slds-backdrop
slds-backdrop_open"
onclick={closeModal}></div>
</template>
</template>
import { LightningElement, api }
from 'lwc';
/* Engagement JSON endpoint */
const ENGAGEMENT_URL =
'https://cloud.e.example.com/engagement';
/* HTML preview endpoint */
const PREVIEW_URL =
'https://cloud.e.example.com/emailpreview';
const COLUMNS = [
{ label: 'Email Name',
fieldName: 'name',
type: 'text' },
{ label: 'Sent',
fieldName: 'sent',
type: 'date',
typeAttributes: {
year: 'numeric',
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit' } },
{ label: 'Opened',
fieldName: 'opened',
type: 'boolean' },
{ label: 'Clicked',
fieldName: 'clicked',
type: 'boolean' },
{ label: 'Bounced',
fieldName: 'bounced',
type: 'boolean' },
{ type: 'action',
typeAttributes: {
rowActions: [
{ label: 'View Email',
name: 'preview' }
] } }
];
export default class EmailEngagement
extends LightningElement {
@api recordId;
emails = [];
columns = COLUMNS;
hasEmails = false;
// Modal state
showModal = false;
previewUrl = '';
previewTitle = '';
connectedCallback() {
this.loadEngagement();
}
async loadEngagement() {
try {
const url = ENGAGEMENT_URL
+ '?id=' + this.recordId;
const resp = await fetch(url);
const data = await resp.json();
if (data.Emails
&& data.Emails.length > 0) {
this.emails = data.Emails
.map((e, i) => ({
...e,
id: String(i)
}));
this.hasEmails = true;
}
} catch (err) {
console.error(
'Failed to load engagement:',
err);
}
}
handleRowAction(event) {
const action = event.detail.action;
const row = event.detail.row;
if (action.name === 'preview'
&& row.archiveKey) {
this.previewUrl = PREVIEW_URL
+ '?key='
+ encodeURIComponent(
row.archiveKey);
this.previewTitle = row.name;
this.showModal = true;
}
}
closeModal() {
this.showModal = false;
this.previewUrl = '';
}
}
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle
xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>59.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>
lightning__RecordPage
</target>
</targets>
<targetConfigs>
<targetConfig
targets="lightning__RecordPage">
<objects>
<object>Contact</object>
<object>Lead</object>
</objects>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
_________________________________________________________________________________
Written by: Lukas Lunow | Forum Ambassador
Lukas Lunow is a Lead Salesforce Consultant at NoA Ignite Denmark, a Salesforce Marketing Champion, and a Trailblazer Community Forum Ambassador. With 6+ years at Salesforce as a Senior Solutions Architect (EMEA), Lukas specialises in Marketing Cloud architecture, deliverability, and cross-cloud integration. He holds every available Marketing Cloud certification including the B2C Solution Architect credential.
Submissions reflect only the opinions of the user who made available the Submission and not the opinions of Salesforce, regardless of whether the user is affiliated with Salesforce, and may contain or constitute products, services, information, data, content and other materials made available by or on behalf of third parties (“Third Party Materials”). Salesforce neither controls nor endorses, nor is Salesforce responsible for, any Third Party Materials, including their accuracy, validity, timeliness, completeness, reliability, integrity, quality, legality, usefulness or safety, or any applicable intellectual property rights. Any Submissions made available through any message board or forum in response to posted questions, or that otherwise purports to answer any questions, including any questions about Salesforce or Programs, are made available for your general knowledge only and should never be relied upon as answers to your specific questions (even if an answer is marked as a “best” answer or with any similar qualifications). You should always contact Salesforce support for answers to your specific questions. Salesforce has no control over Submissions, and is not responsible for any use or misuse (including any distribution) by any third party of Submissions.
If you have questions, tap into the wisdom of our entire Trailblazer Community here: https://trailhead.salesforce.com/trailblazer-community/feed
005318897

We use three kinds of cookies on our websites: required, functional, and advertising. You can choose whether functional and advertising cookies apply. Click on the different cookie categories to find out more about each category and to change the default settings.
Privacy Statement
Required cookies are necessary for basic website functionality. Some examples include: session cookies needed to transmit the website, authentication cookies, and security cookies.
Functional cookies enhance functions, performance, and services on the website. Some examples include: cookies used to analyze site traffic, cookies used for market research, and cookies used to display advertising that is not directed to a particular individual.
Advertising cookies track activity across websites in order to understand a viewer’s interests, and direct them specific marketing. Some examples include: cookies used for remarketing, or interest-based advertising.