<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: AMPscript Ninja</title>
    <description>The latest articles on DEV Community by AMPscript Ninja (@ampscript-ninja).</description>
    <link>https://dev.to/ampscript-ninja</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1914879%2F24506741-0c3c-48d4-a5a7-6a0282037857.jpg</url>
      <title>DEV Community: AMPscript Ninja</title>
      <link>https://dev.to/ampscript-ninja</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ampscript-ninja"/>
    <language>en</language>
    <item>
      <title>Processing Unsubscribes With Nothing But Email Address In SFMC Engagement</title>
      <dc:creator>AMPscript Ninja</dc:creator>
      <pubDate>Wed, 25 Feb 2026 03:08:59 +0000</pubDate>
      <link>https://dev.to/ampscript-ninja/processing-unsubscribes-with-nothing-but-email-address-in-sfmc-engagement-28gl</link>
      <guid>https://dev.to/ampscript-ninja/processing-unsubscribes-with-nothing-but-email-address-in-sfmc-engagement-28gl</guid>
      <description>&lt;p&gt;As an email marketer, I'm no stranger to random unsubscribe requests with nothing but an email address. Sometimes it's from customer service after someone called/emailed asking... sometimes it's from the legal/privacy team with a customer wanting their data deleted and all communications stopped... sometimes it's a random employee whose family member or neighbor says we send too many emails.&lt;/p&gt;

&lt;p&gt;Or, as explained in my previous article, sometimes it's because one of the planet's largest email platforms decided to expire all historical links, so now your unsubscribe page doesn't work and you needed to create an emergency page to capture opt-out requests.&lt;/p&gt;

&lt;p&gt;Read more:&lt;br&gt;
&lt;a href="https://ampscript-ninja.hashnode.dev/how-to-manage-expired-links-in-sfmc" rel="noopener noreferrer"&gt;https://ampscript-ninja.hashnode.dev/how-to-manage-expired-links-in-sfmc&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Whatever your use case is, following are some easy options for manually processing unsubscribes.&lt;/p&gt;


&lt;h2&gt;
  
  
  Emergency Opt-Out Page
&lt;/h2&gt;

&lt;p&gt;Hot on the heels of SFMC's link expiration issues, let's talk about how to capture opt-out requests, and having your link expiration page have a link to a basic CloudPage is a great solution for this.&lt;/p&gt;

&lt;p&gt;First, you could do a Smart Capture form, which is their drag-and-drop form option that loads to a data extension and optionally triggers an email out of Journey Builder.&lt;/p&gt;

&lt;p&gt;Salesforce Help:&lt;br&gt;
&lt;a href="https://help.salesforce.com/s/articleView?id=mktg.mc_cp_create_a_smart_capture_form_in_cloudpages.htm&amp;amp;type=5" rel="noopener noreferrer"&gt;https://help.salesforce.com/s/articleView?id=mktg.mc_cp_create_a_smart_capture_form_in_cloudpages.htm&amp;amp;type=5&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Second, you build your own basic HTML form and use the AMPscript &lt;code&gt;InsertData()&lt;/code&gt; function to record submissions to a data extension.&lt;/p&gt;

&lt;p&gt;Tim Ziter with Hands On SFMC outlines it well here:&lt;br&gt;
&lt;a href="https://handsonsfmc.com/building-a-basic-form-in-a-cloud-page/" rel="noopener noreferrer"&gt;https://handsonsfmc.com/building-a-basic-form-in-a-cloud-page/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For my own page, I went with the second option - a basic custom form asking for only an email address and posting it to a data extension. I already had a custom unsubscribe page hosted in CloudPages, with error logging to a data extension for page loads without all of the required information (subscriber key, email address, Publication List ID, Job ID, etc.), so it was relatively easy to add a fallback version where if the page loads with no information, it asks for email address and records it to my existing data extension.&lt;/p&gt;

&lt;p&gt;Whichever approach you take, here are some guidelines when setting up your data extension - these settings are important later on for my Subscription Center solution.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Create a &lt;strong&gt;subscriber_key&lt;/strong&gt; field that is optional/nullable since your page won't populate it, but you'll add it later (this is important)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create an &lt;strong&gt;email&lt;/strong&gt; field for the page to write to&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mark your data extension as &lt;strong&gt;Used For Sending: Yes&lt;/strong&gt; and &lt;strong&gt;Used For Testing: Yes&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Managing Unsubscribes
&lt;/h2&gt;

&lt;p&gt;Once you have a basic CloudPage recording email address to a data extension for manual follow-up, you have several options to process them. Let's walk through those options, starting with the simplest.&lt;/p&gt;
&lt;h3&gt;
  
  
  Low Complexity: Manage Directly In All Subscribers
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Open your data extension in &lt;strong&gt;Contact Builder&lt;/strong&gt; - this allows you to edit records if you need to add information, or delete once you've successfully processed the request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In a second tab, open &lt;strong&gt;Email Studio &amp;gt; Subscribers &amp;gt; All Subscribers&lt;/strong&gt; and click &lt;strong&gt;Search&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This will show the subscriber key(s) associated with that email address. Selecting one, clicking &lt;strong&gt;View Properties&lt;/strong&gt;, and going to the &lt;strong&gt;Lists&lt;/strong&gt; tab will show that subscriber's &lt;strong&gt;Publication Lists&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Selecting a list, clicking &lt;strong&gt;Details&lt;/strong&gt;, and then clicking &lt;strong&gt;Unsubscribe&lt;/strong&gt; will change the subscriber's status on that list to unsubscribed, preventing future sends of emails with that publication list selected.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;My Opinion:&lt;/strong&gt; This is the easiest option, but not the best. I won't say authoritatively that unsubscribes with this method aren't shared, but in my own personal testing, unsubscribes did not consistently, reliably appear in the &lt;strong&gt;Unsubs&lt;/strong&gt; tracking extract. If you're sharing unsubscribes with CRM or another system with tracking extracts, &lt;strong&gt;then I don't recommend this option.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Low Complexity: Use The Existing Subscription Center
&lt;/h3&gt;

&lt;p&gt;If you're already using SFMC's default Subscription Center links in your emails, you can use that to manage Publication Lists for a specific subscriber.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Like before, open your data extension in &lt;strong&gt;Contact Builder&lt;/strong&gt; in one tab and &lt;strong&gt;All Subscribers&lt;/strong&gt; in another.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Search the email address in &lt;strong&gt;All Subscribers&lt;/strong&gt; to get the &lt;strong&gt;subscriber key&lt;/strong&gt;. Copy the subscriber key, select the data extension record in &lt;strong&gt;Contact Builder&lt;/strong&gt;, click &lt;strong&gt;Edit&lt;/strong&gt;, paste the subscriber key in the appropriate field (remember from above? you already set that up as an optional field), and &lt;strong&gt;Save&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Now that your data extension has both subscriber key and email address, you can use it to preview an email. Open &lt;strong&gt;Content Builder&lt;/strong&gt; in a third tab. You can use any existing email with the default Subscription Center link, but to avoid manual unsubscribes from being attributed to a real email in reporting, I recommend you create a separate email template specifically for managing unsubscribes.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All you need is to &lt;code&gt;set&lt;/code&gt; your subscriber key and email address, and add the Subscription Center link.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;%%[
/* STARTING INFO FROM DATA EXTENSION */
    set @subscriber_key = subscriber_key
    set @email = email
]%%

Open the &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"%%subscription_center_url%%"&lt;/span&gt; &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"_blank"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Subscription Center&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Preview your email using the record in your data extension, and click the Subscription Center link... but wait, you can't do that from a preview inside the email platform! You'll see a link that just says: &lt;code&gt;javascript:void(0)&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Not to worry, we can get around this with a test send. Before you fire your test send, make sure the very last checkbox, &lt;strong&gt;Enable System Generated Links&lt;/strong&gt;, is checked. This will generate SFMC system links, including the one to Subscription Center. Send your test email.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Go to your inbox, open your email, and click the Subscription Center link - you're now seeing it &lt;em&gt;as that subscriber&lt;/em&gt;, exactly as if they'd clicked the link themselves. You can now remove them from Publication Lists or &lt;strong&gt;Unsubscribe From All&lt;/strong&gt; as needed.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;My Opinion:&lt;/strong&gt; This is the easiest, most reliable option that doesn't require a custom AMPscript development.&lt;/p&gt;

&lt;h3&gt;
  
  
  Medium Complexity: Link To Your Custom Unsubscribe Page
&lt;/h3&gt;

&lt;p&gt;If you already have a custom unsubscribe page or preference center, you're probably linking to that page with the AMPscript &lt;code&gt;CloudPagesURL()&lt;/code&gt; function to pass subscriber info (subscriber key and email address), publication info (Publication List ID, maybe a brand or content type), and probably email info (Job ID, Batch ID). That page likely uses AMPscript or another method to call the Salesforce SOAP API and post a &lt;a href="https://sfmarketing.cloud/2019/10/06/unsubscribe-and-log-an-unsubevent-with-a-logunsubevent-execute-call/" rel="noopener noreferrer"&gt;Log Unsub Event&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Instead of creating a way into the Subscription Center for the specified subscriber, you'll create the necessary link(s) into your existing page(s) instead.&lt;/p&gt;

&lt;p&gt;The first few steps are the same - get the email address from the data extension, search it in &lt;strong&gt;All Subscribers&lt;/strong&gt; to get the subscriber key, and enter the subscriber key in the data extension so it can be used to preview an email.&lt;/p&gt;

&lt;p&gt;If you have an all-in-one custom preference center, then your email template just needs to have a fully constructed &lt;code&gt;CloudPagesURL()&lt;/code&gt; function to your preference center page. When you preview as the subscriber in your data extension, you should be able to click into the preference center and make changes exactly as that subscriber would.&lt;/p&gt;

&lt;p&gt;If you have a simpler unsubscribe page that only processes unsubscribes from a single Publication List, then your email template can have multiple links.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;In your custom email, you'll use an AMPscript data extension lookup against the &lt;a href="https://help.salesforce.com/s/articleView?id=mktg.mc_as_data_view_listsubscribers.htm&amp;amp;type=5" rel="noopener noreferrer"&gt;&lt;strong&gt;_ListSubscribers&lt;/strong&gt; data view&lt;/a&gt; to see all Publication Lists by status.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In a &lt;code&gt;for&lt;/code&gt; loop, go through each Publication List, and add the necessary information you need to generate a link to your custom unsubscribe page.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That might look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;%%[
    /* Lookup all Publication Lists from the _ListSubscribers data view, ordered by 'ListName', matching on 'SubscriberKey'. */
    set @publication_lists_rows = LookupOrderedRows('_ListSubscribers',0,'ListName asc','SubscriberKey',@subscriber_key)

    /* If there are results, show them below. */
    if RowCount(@publication_lists_rows) &amp;gt;= 1 then
        set @show_lists_results = true
    endif
]%%

%%[if @show_lists_results == true then]%%
    &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"600"&lt;/span&gt; &lt;span class="na"&gt;align=&lt;/span&gt;&lt;span class="s"&gt;"left"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;table&lt;/span&gt; &lt;span class="na"&gt;align=&lt;/span&gt;&lt;span class="s"&gt;"left"&lt;/span&gt; &lt;span class="na"&gt;cellspacing=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;cellpadding=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;border=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Publication List &lt;span class="ni"&gt;&amp;amp;nbsp;&amp;amp;nbsp;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;

                    &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;ID &lt;span class="ni"&gt;&amp;amp;nbsp;&amp;amp;nbsp;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;

                    &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Status &lt;span class="ni"&gt;&amp;amp;nbsp;&amp;amp;nbsp;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;

                    &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Link&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;

                %%[
                    /* Loop through the Publication Lists to get the list ID, name, and status. */
                    for @l = 1 to RowCount(@publication_lists_rows) do
                        set @publication_list_match = Row(@publication_lists_rows,@l)
                        set @l_list_id = Field(@publication_list_match,'ListID')
                        set @l_list_name = Field(@publication_list_match,'ListName')
                        set @l_list_status = Propercase(Field(@publication_list_match,'Status'))
                        set @l_email = Field(@publication_list_match,'EmailAddress')

                        /* Based on the Publication List name, set additional variables as needed to be able to construct the unsubscribe URL. */
                            set @l_brand = ''
                            set @unsub_link = ''

                            if IndexOf(@l_list_name,'Example') != 0 then
                                set @l_brand = 'EXA'
                                set @unsub_page_id = '1000'
                            elseif IndexOf(@l_list_name,'Sample') != 0 then
                                set @l_brand = 'SAM'
                                set @unsub_page_id = '2000'
                            endif

                        /* Construct the unsubscribe link. */
                            if @l_list_status == 'Active' and not Empty(@l_brand) then
                                set @job_id = 0
                                set @batch_id = 0
                                set @unsub_brand = @l_brand
                                set @unsub_link = CloudPagesURL(@unsub_page_id,'subscriber_key',@subscriber_key,'email',@l_email,'job_id',@job_id,'batch_id',@batch_id,'unsub_brand',@unsub_brand)
                            endif
                ]%%

                &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;
                        %%=v(@l_list_name)=%% &lt;span class="ni"&gt;&amp;amp;nbsp;&amp;amp;nbsp;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;

                    &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;
                        %%=v(@l_list_id)=%% &lt;span class="ni"&gt;&amp;amp;nbsp;&amp;amp;nbsp;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;

                    &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;
                        %%=v(@l_list_status)=%% &lt;span class="ni"&gt;&amp;amp;nbsp;&amp;amp;nbsp;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;

                    &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;
                        %%[if not Empty(@unsub_link) then]%%&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"link link-ext"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"%%=RedirectTo(@unsub_link)=%%"&lt;/span&gt; &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"_blank"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Unsubscribe&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;%%[endif]%%
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;

                %%[next @l]%%

            &lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
%%[endif]%%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should result in a list of Publication Lists, with status, and if the status is Active, a link out to the appropriate unsubscribe page for that subscriber.&lt;/p&gt;

&lt;h3&gt;
  
  
  Highest Complexity: Fully Custom One-Click Solution
&lt;/h3&gt;

&lt;p&gt;If you're looking for an all-in-one solution that eliminates all manual work, the only limit with AMPscript is your imagination. Here is an outline of my current custom solution.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;I have a secure CloudPage that only select users can access.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A form page searches an email address, and uses &lt;a href="https://medium.com/@b2.shashi/how-to-retrieve-all-subscriberkeys-associated-with-an-email-address-using-ampscript-7019fbf39d36" rel="noopener noreferrer"&gt;this solution by Shashi Prasad&lt;/a&gt; to search all Subscriber Keys associated with an email address, preventing the need to search in All Subscribers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For each subscriber key returned, I use the AMPscript above to search all Publication Lists and generate all the unsubscribe URLs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Net result: I have one page with all list statuses and all unsubscribe links, in one place, from a single email address copy/paste.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Of course, that can still be a fair amount of clicking to process all those unsubscribes, so I'd like to enhance this further someday:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The first page would use a data extension look up to list all email addresses currently in the data extension for follow-up. Clicking an email address would open the current page in a new tab, which would eliminate the copy/paste step.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Following all of the lists-by-subscribers results, an &lt;strong&gt;Unsubscribe From All&lt;/strong&gt; button would reload the page, fire a LogUnsubEvent for every active Publication List for each subscriber key, and save and display all the results on screen.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you wanted to &lt;strong&gt;&lt;em&gt;get crazy&lt;/em&gt;&lt;/strong&gt;, you could eliminate the human step altogether and have a custom unsubscribe page do all of this automatically as soon as someone enters an email address. That would be extremely efficient. Personally, I prefer the human review element for something like this to protect against abuse or fraud, but I'd like the human steps to be as fast and as few clicks as possible.&lt;/p&gt;




&lt;p&gt;Ultimately, the best solution for each team is the solution they can efficiently execute themselves. I hope this post provided some easy options (or thought starters on complex options) to get you started, and I'd love to hear additional ideas for making the process as efficient as possible.&lt;/p&gt;

</description>
      <category>sfmc</category>
      <category>ampscript</category>
    </item>
    <item>
      <title>How To Manage Expired Links In SFMC Engagement</title>
      <dc:creator>AMPscript Ninja</dc:creator>
      <pubDate>Sat, 07 Feb 2026 02:38:50 +0000</pubDate>
      <link>https://dev.to/ampscript-ninja/how-to-manage-expired-links-in-sfmc-engagement-2p15</link>
      <guid>https://dev.to/ampscript-ninja/how-to-manage-expired-links-in-sfmc-engagement-2p15</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Updated February 2026 with more details and examples&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What happens to a link in your email after it expires? Did you even know that links in emails do expire?&lt;/p&gt;

&lt;p&gt;These questions are front of mind for a lot of SFMC Engagement users after… what are we calling it? The Linkpocalypse? Expiregate? If you have a punny idea for the name, please comment!&lt;/p&gt;

&lt;p&gt;On January 23, 2026, Salesforce sent an email announcing that they had upgraded the security of all of their links with enhanced encryption — and along the way, they expired all links in all emails sent prior to January 21, 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Yes, you read that right — all links in all emails sent prior to January 21, 2026 are now expired.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Including unsubscribe links, by the way, so now we're all out of compliance with CAN-SPAM and other countries' legislation.&lt;/p&gt;

&lt;p&gt;To understand the impact, let’s take a quick refresher look at how links in emails actually work, and what we can do for damage control.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Links Work
&lt;/h2&gt;

&lt;p&gt;When you’re building an email, you define the URL where you want each CTA and other clickable elements to link to. When you send that email, SFMC encrypts your original links, stores its encrypted versions and your original versions in a links table, and delivers your email with the encrypted versions. If you look at a link in your inbox (hover on a computer, or long-press on a phone), you’ll see that it’s in a different format than what you originally added:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://click.[subdomain].[domain].com?qs=[reallylongencryptedstring]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;subdomain.domain&lt;/code&gt; is likely something related to ExactTarget, or your own domain that you defined if you have a Sender Authentication Package (SAP).&lt;/p&gt;

&lt;p&gt;When a subscriber clicks/taps your CTA, a few things happen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This SFMC-generated &lt;code&gt;click.___&lt;/code&gt; URL hits SFMC’s servers&lt;/li&gt;
&lt;li&gt;SFMC decrypts the encrypted link to know which CTA in which email was clicked by which subscriber, and records it all for reporting&lt;/li&gt;
&lt;li&gt;SFMC looks up the original URL you specified and redirects there&lt;/li&gt;
&lt;li&gt;The subscriber lands on your original URL, and it happens so quickly they probably never even noticed the URL changing during the redirect&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Essentially, the SFMC URL redirects to your URL, and that redirect powers how click tracking works.&lt;/p&gt;

&lt;p&gt;Until…&lt;/p&gt;

&lt;h2&gt;
  
  
  Expired Link Handling
&lt;/h2&gt;

&lt;p&gt;Eventually, links expire — which makes sense, because SFMC doesn’t want to store their encrypted URL and your original URL as a reference forever. You have direct control over how long SFMC keeps links active before expiring them — a minimum of 60 days and a maximum of 2 years.&lt;/p&gt;

&lt;p&gt;There are two ways to get to these settings (both are identical):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Email Studio &amp;gt; Admin &amp;gt; Send Management &amp;gt; URL Expiration&lt;/li&gt;
&lt;li&gt;Setup &amp;gt; Platform Tools &amp;gt; Feature Settings &amp;gt; Email Studio &amp;gt; URL Expiration&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What you’ll notice is that there is also an option to define where you want expired links to go. If you keep the &lt;strong&gt;System Default URL&lt;/strong&gt; selected, it will go to a blank white page (much like a 404) with this generic message:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This link has expired. Please contact the sender of the email for more information.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://cl.exct.net/expired.html" rel="noopener noreferrer"&gt;https://cl.exct.net/expired.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not only is that unhelpful, it’s not remotely compliant for someone who just clicked an unsubscribe link.&lt;/p&gt;

&lt;p&gt;Alternatively, if you select the &lt;strong&gt;Custom Defined URL&lt;/strong&gt;, you can decide where you want it to go.&lt;/p&gt;

&lt;p&gt;If you have an enterprise plan, these settings are configurable two ways: org-wide from your parent business unit, or individually for each child business unit, so you can define different expired link URLs for each business unit if you prefer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isn’t It Too Late?
&lt;/h2&gt;

&lt;p&gt;Now that SFMC expired all of our links, isn’t it too late to do anything about it? Or will changing the &lt;strong&gt;Custom Defined URL&lt;/strong&gt; work retroactively?&lt;/p&gt;

&lt;p&gt;I wasn’t sure, so I tested it. I changed the URL to my company's home page, clicked Save, went to an old email in my inbox, held my breath, clicked a link, and…&lt;/p&gt;

&lt;p&gt;…was disappointed. I landed on the generic expiration page.&lt;/p&gt;

&lt;p&gt;So I tried again. And again. About 15 times over the next 5 minutes, probably, and eventually, &lt;strong&gt;it actually worked&lt;/strong&gt;. A link in an old email redirected to the new page I defined.&lt;/p&gt;

&lt;p&gt;Here’s what I conclude:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When someone clicks an expired link, SFMC checks what URL is currently defined, not at the time the email was sent.

&lt;ul&gt;
&lt;li&gt;And in retrospect, that makes sense — if the original point of expiring links was to alleviate long-term link-maps-to-link storage, then storing the expiration link URL long-term completely defeats the purpose.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;However that check works, it must be cached in some way, because it takes a few minutes for the change to take effect.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;So, we’ve proven that A) you can change where your expired links go, and B) it applies retroactively. Where should you send your expired links?&lt;/p&gt;

&lt;h2&gt;
  
  
  Considerations
&lt;/h2&gt;

&lt;p&gt;There are a lot of directions you can go with this. Following is an outline of thought starters and how I approached it for &lt;a href="https://www.polaris.com/en-us/" rel="noopener noreferrer"&gt;Polaris&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;First, you should think about compliance.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How important is capturing unsubscribes?

&lt;ul&gt;
&lt;li&gt;We wanted to stay compliant with USA’s CAN-SPAM and Canada’s CASL, so a path to unsubscribe was a top priority for me.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Do you have any other content with legal requirements or other sensitivities?

&lt;ul&gt;
&lt;li&gt;We have one business unit that sends notifications about product recalls — this is a very sensitive topic and consumer safety is our #1 priority, so we wanted to ensure anyone clicking a product recall email got to a web page where they could quickly, easily get all the information they need.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Second, you should think about organization needs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Since you can define different URLs for different business units, do your BUs have different needs? This is really useful if you use different BUs for different companies/brands, different countries/regions, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Third, you should think about user experience.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What types of emails do you send, and what types of CTAs do you include? Do you know what CTAs are most clicked a period of time after the send?

&lt;ul&gt;
&lt;li&gt;We wanted to prioritize top help CTAs (like contacting a dealership or customer service), and we also know that one particular CTA that gets a lot of repeat clicks over time is online order tracking, so we wanted to prioritize that as well.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Are all the top actions/CTAs represented on your company’s home page? If so, maybe you can simply set your website’s home page as your expiration page. Or, do you have more than one home page? In that case, you may need to consider something custom.

&lt;ul&gt;
&lt;li&gt;For Polaris, we send emails from about a dozen brands to USA and Canada, so there was no one page on our website that represented all of those destinations.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;What’s your tone? Not getting to where you were expecting when clicking an email can be frustrating, so is there an opportunity to inject some humor?

&lt;ul&gt;
&lt;li&gt;One of our early drafts had a photo of some hunters standing next to an off road vehicle (one of our top products), looking out over a hillside with binoculars, accompanied by the tagline “Now Where Did That Link Go?” We thought it was funny but were overridden by humorless marketing managers.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  My Solution
&lt;/h2&gt;

&lt;p&gt;Ultimately, I determined that the only way to represent all of our brands and multiple CTAs, and to incorporate an unsubscribe option, was to create a custom landing page with CloudPages. I’m not a web developer and not much more than a hack at Bootstrap, so it’s not elegant. But it’s not bad for turning around in less than a business day, and it does the job of catching people coming from a wide range of content and providing links to a lot of different destinations.&lt;/p&gt;

&lt;p&gt;If you’re curious: &lt;a href="https://cloud.hello.polaris.com/expired-email-link-STAGE" rel="noopener noreferrer"&gt;https://cloud.hello.polaris.com/expired-email-link-STAGE&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I was also able to incorporate two pieces of functionality that I felt were important.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The country picker at the top is actually a form element that refreshes the page with an added URL parameter. The AMPscript on the page reads the culture code from that URL parameter and adjusts all of the page’s links, which are dynamic — so if &lt;strong&gt;United States&lt;/strong&gt; is selected, all links go to our &lt;code&gt;/en-us/&lt;/code&gt; (English USA) site, or if &lt;strong&gt;Canada&lt;/strong&gt; is selected, all links go to our &lt;code&gt;/en-ca/&lt;/code&gt; (English Canada) site.&lt;/li&gt;
&lt;li&gt;The unsubscribe link at the bottom links to another CloudPage, my custom unsubscribe page. Typically, a link from one of our emails passes information about the subscriber, the email, the brand, and the publication list, and when they click an Unsubscribe button to confirm, it processes the unsubscribe. In this case, I have none of that info, so I created a fallback version that engages if there is no information passed — it just asks for an email address, which is then logged to a data extension for me to follow up later. Do I love that I need to go search that person manually in All Subscribers and process the unsubscribes? No. But hey, it’s technically compliant if I do it within 10 days.

&lt;ul&gt;
&lt;li&gt;Check out this follow-up article with options for manually processing unsubscribes: &lt;a href="https://dev.to/ampscript-ninja/processing-unsubscribes-with-nothing-but-email-address-in-sfmc-engagement-28gl"&gt;https://dev.to/ampscript-ninja/processing-unsubscribes-with-nothing-but-email-address-in-sfmc-engagement-28gl&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What’s Your Solution?
&lt;/h2&gt;

&lt;p&gt;I hope you found this helpful. If you have other ideas/considerations for a great user experience when clicking an expired link — well, “great” is optimistic, let’s settle for “less bad” — I’d love to hear about it in the comments.&lt;/p&gt;

</description>
      <category>sfmc</category>
    </item>
    <item>
      <title>Calculating A Percent vs. A Goal With SFMC's AMPscript</title>
      <dc:creator>AMPscript Ninja</dc:creator>
      <pubDate>Fri, 23 Jan 2026 03:06:39 +0000</pubDate>
      <link>https://dev.to/ampscript-ninja/calculating-a-percent-vs-a-goal-with-sfmcs-ampscript-3j4n</link>
      <guid>https://dev.to/ampscript-ninja/calculating-a-percent-vs-a-goal-with-sfmcs-ampscript-3j4n</guid>
      <description>&lt;p&gt;How many year-end review emails did you get this year? Ever since Spotify Wrapped first made waves, it feels like more and more brands have tried to recreate the same hyper-personalized feel, my company included. One of the questions that came up from my team this year:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can we calculate a percentage comparing someone’s actual progress this year against a goal, or against their actual progress last year?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Absolutely!&lt;/strong&gt; In this article I’ll give you all the AMPscript you need. And to have even more fun, I’ll show you how to reuse those percentages in dynamically sized HTML progress bars.&lt;/p&gt;

&lt;h2&gt;
  
  
  Percentages In AMPscript
&lt;/h2&gt;

&lt;p&gt;The first thing we’ll do is select what is being compared — progress vs. a goal, this year vs. last year, personal performance vs. community average, etc. This can be anything you want as long as it’s a number format.&lt;/p&gt;

&lt;p&gt;To keep things simple for this example, I’m going to do two sets of progress vs. a goal, and for example purposes I will just hard code some values — one under goal, one over.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%%[
    /* GOAL 1 */
        set @actual1 = "421"
        set @goal1 = "500"

    /* GOAL 2 */
        set @actual2 = "523"
        set @goal2 = "500"
]%%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, we’ll use &lt;code&gt;Divide()&lt;/code&gt; to get the progress as a decimal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set @percentage1 = Divide(@actual1,@goal1)
set @percentage2 = Divide(@actual2,@goal2)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Results: 421 / 500 = &lt;strong&gt;0.842&lt;/strong&gt;, and 523 / 500 = &lt;strong&gt;1.046&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Third, we’re going to use &lt;code&gt;Multiply()&lt;/code&gt; by 100 to turn our decimals into percentages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set @percentage1 = Multiply(@percentage1,100)
set @percentage2 = Multiply(@percentage2,100)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(You’ll notice I’m using &lt;code&gt;set&lt;/code&gt; on the same variable again — that is supported, if you want to do that to make iterative changes to the same variable. When we get to the end, I’ll give you a single consolidated line with all transformations, but for now let’s keep the steps separate so we can see what each one is doing.)&lt;/p&gt;

&lt;p&gt;Results: 0.842 × 100 = &lt;strong&gt;84.2&lt;/strong&gt;, and 1.046 × 100 = &lt;strong&gt;104.6&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We’re getting close, but depending on how large your numbers are, you may end up with a lot of decimal points — so fourth, let’s reformat as a whole number (‘N’) with zero decimal points (‘0’).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set @percentage1 = FormatNumber(@percentage1,'N0')
set @percentage2 = FormatNumber(@percentage2,'N0')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This rounds us to a clean &lt;strong&gt;84&lt;/strong&gt; and &lt;strong&gt;105&lt;/strong&gt;, perfect for showing percentages.&lt;/p&gt;

&lt;p&gt;At this point you can use this in your email content; e.g. “You completed &lt;strong&gt;84%&lt;/strong&gt; of your goal”, but for positive numbers “You completed &lt;strong&gt;105%&lt;/strong&gt; of your goal” feels a little odd. It feels more natural to use a percentage beat or missed, so let’s calculate that next.&lt;/p&gt;

&lt;p&gt;We’ll use &lt;code&gt;Subtract()&lt;/code&gt; to take away 100, which will leave us with a positive or negative number less than 100.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set @percentage_beat1 = Subtract(@percentage1,100)
set @percentage_beat2 = Subtract(@percentage2,100)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Results: 84 - 100 = &lt;strong&gt;-16&lt;/strong&gt;, and 105 - 100 = &lt;strong&gt;5&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After a quick &lt;code&gt;Replace()&lt;/code&gt; to strip off the &lt;code&gt;-&lt;/code&gt; (if present)…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set @percentage_beat1 = Replace(@percentage_beat1,'-','')
set @percentage_beat2 = Replace(@percentage_beat2,'-','')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…we’re left with the final numbers, &lt;strong&gt;16&lt;/strong&gt; (percent) under, and &lt;strong&gt;5&lt;/strong&gt; (percent) over.&lt;/p&gt;

&lt;p&gt;Before we get into how to use those final numbers in the email content, here is the complete view of all these steps together.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%%[
/* PERCENTAGE TO GOAL */
    /* GOAL 1 */
        set @actual1 = "421"
        set @goal1 = "500"

        set @percentage1 = Divide(@actual1,@goal1)                   /* Divide the actual vs. the goal */
        set @percentage1 = Multiply(@percentage1,100)                /* Multiply by 100 for a percent */
        set @percentage1 = FormatNumber(@percentage1,'N0')           /* Format as a whole number 'N' with 0 decimal points */
        set @percentage1 = Replace(@percentage1,',','')              /* Remove commas if exceeding 1000 */

        set @percentage_beat1 = Subtract(@percentage1,100)           /* Subtract 100 to get over/under */
        set @percentage_beat1 = Replace(@percentage_beat1,'-','')    /* Remove the '-' sign, if negative */
]%%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And as promised, here is the consolidated version, with all of the functions working in layers from the inside to the outside.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/* GOAL 3 */
        set @actual3 = "421"
        set @goal3 = "500"

        set @percentage3 = Replace(FormatNumber(Multiply(Divide(@actual3,@goal3),100),'N0'),',','')
        set @percentage_beat3 = Replace(Subtract(@percentage3,100),'-','')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Presenting The Results
&lt;/h2&gt;

&lt;p&gt;Let’s evaluate those results to show a summary message. We’ve got 4 possible scenarios:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The goal was met at &lt;em&gt;exactly&lt;/em&gt; 100% (&lt;code&gt;==&lt;/code&gt; is equal to)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The goal was beat, over 100% (&lt;code&gt;&amp;gt;&lt;/code&gt; is greater than)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The goal was missed, below 100% (&lt;code&gt;&amp;lt;&lt;/code&gt; is less than)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The goal had no progress at all (&lt;code&gt;else&lt;/code&gt; default)&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So let’s hit some if/then statements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if @percentage1 == 100 then
    set @result1 = 'You met your goal &amp;amp;ndash; great work!'
elseif @percentage1 &amp;gt; 100 then
    set @result1 = Concat('You beat your goal by ',@percentage_beat1,'% &amp;amp;ndash; nice work!')
elseif @percentage1 &amp;lt; 100 then
    set @result1 = Concat('You missed your goal by ',@percentage_beat1,'% &amp;amp;ndash; try again next year!')
else
    set @result1 = 'You may not have worked toward this goal this year, but try again next year!'
endif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows the summary sentence about each goal’s progress to be dynamic based on their results.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dynamic HTML Progress Bars
&lt;/h2&gt;

&lt;p&gt;Just for fun, let’s briefly play with one of my favorite AMPscript concepts — using AMPscript inside HTML/CSS. Since we have variables with percentages, we can use those variables for percentage-based HTML table cell widths to create a status bar that is partially or fully colored based on goal progress.&lt;/p&gt;

&lt;p&gt;Here’s a picture of my proof of concept to help you visualize what we’re building:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feg78df6za3kmt1dl5xxo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feg78df6za3kmt1dl5xxo.png" alt="screenshot" width="630" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First, we’ll evaluate the goal progress to create variable colors and widths.&lt;/p&gt;

&lt;p&gt;If it’s at/above 100, then we’ll:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apply a green color&lt;/li&gt;
&lt;li&gt;Set the width of the complete bar to an even 100&lt;/li&gt;
&lt;li&gt;Set the width of the incomplete bar to 0, and hide it later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it’s below 100, then we’ll:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apply a blue color&lt;/li&gt;
&lt;li&gt;Set the width of the complete bar to the actual progress percentage&lt;/li&gt;
&lt;li&gt;Set the width of the incomplete bar to the remainder (100 minus progress)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%%[
    /* STATUS BARS */
        if @percentage1 &amp;gt;= 100 then
            set @percentage1_color = '#009933'
            set @percentage1_complete_width = 100
            set @percentage1_incomplete_width = 0
        else
            set @percentage1_color = '#0099ff'
            set @percentage1_complete_width = @percentage1
            set @percentage1_incomplete_width = Subtract(100,@percentage1)
        endif

        if @percentage2 &amp;gt;= 100 then
            set @percentage2_color = '#009933'
            set @percentage2_complete_width = 100
            set @percentage2_incomplete_width = 0
        else
            set @percentage2_color = '#0099ff'
            set @percentage2_complete_width = @percentage2
            set @percentage2_incomplete_width = Subtract(100,@percentage2)
        endif
]%%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let’s put those variables into some HTML. Notice the &lt;code&gt;td&lt;/code&gt; cells are being populated with dynamic &lt;code&gt;width&lt;/code&gt;, &lt;code&gt;bgcolor&lt;/code&gt;, and &lt;code&gt;background&lt;/code&gt; values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;table&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"600"&lt;/span&gt; &lt;span class="na"&gt;align=&lt;/span&gt;&lt;span class="s"&gt;"left"&lt;/span&gt; &lt;span class="na"&gt;cellspacing=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;cellpadding=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;border=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"600"&lt;/span&gt; &lt;span class="na"&gt;align=&lt;/span&gt;&lt;span class="s"&gt;"left"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;br&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;Goal 1: %%=v(@actual1)=%% vs. %%=v(@goal1)=%% = %%=v(@percentage1)=%%%
            &lt;span class="nt"&gt;&amp;lt;br&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;--&amp;gt; %%=v(@result1)=%%
        &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"600"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;table&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"600"&lt;/span&gt; &lt;span class="na"&gt;align=&lt;/span&gt;&lt;span class="s"&gt;"left"&lt;/span&gt; &lt;span class="na"&gt;cellspacing=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;cellpadding=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;border=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"%%=v(@percentage1_complete_width)=%%%"&lt;/span&gt; &lt;span class="na"&gt;align=&lt;/span&gt;&lt;span class="s"&gt;"right"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        %%=v(@percentage1)=%%%
                        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"font-size: 6px; mso-line-height-rule: exactly; line-height: 6px;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="ni"&gt;&amp;amp;nbsp;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;

                    %%[if @percentage1_incomplete_width &amp;gt; 0 then]%%
                    &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"%%=v(@percentage1_incomplete_width)=%%%"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
                    %%[endif]%%
                &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"%%=v(@percentage1_complete_width)=%%%"&lt;/span&gt; &lt;span class="na"&gt;bgcolor=&lt;/span&gt;&lt;span class="s"&gt;"%%=v(@percentage1_color)=%%"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"background: %%=v(@percentage1_color)=%%;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"font-size: 18px; mso-line-height-rule: exactly; line-height: 18px;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="ni"&gt;&amp;amp;nbsp;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;

                    %%[if @percentage1_incomplete_width &amp;gt; 0 then]%%
                    &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"%%=v(@percentage1_incomplete_width)=%%%"&lt;/span&gt; &lt;span class="na"&gt;bgcolor=&lt;/span&gt;&lt;span class="s"&gt;"#bbbbbb"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"background: #bbbbbb;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"font-size: 18px; mso-line-height-rule: exactly; line-height: 18px;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="ni"&gt;&amp;amp;nbsp;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
                    %%[endif]%%
                &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"600"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"font-size: 18px; mso-line-height-rule: exactly; line-height: 18px;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="ni"&gt;&amp;amp;nbsp;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"600"&lt;/span&gt; &lt;span class="na"&gt;align=&lt;/span&gt;&lt;span class="s"&gt;"left"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;br&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;Goal 2: %%=v(@actual2)=%% vs. %%=v(@goal2)=%% = %%=v(@percentage2)=%%%
            &lt;span class="nt"&gt;&amp;lt;br&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;--&amp;gt; %%=v(@result2)=%%
        &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"600"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;table&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"600"&lt;/span&gt; &lt;span class="na"&gt;align=&lt;/span&gt;&lt;span class="s"&gt;"left"&lt;/span&gt; &lt;span class="na"&gt;cellspacing=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;cellpadding=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;border=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"%%=v(@percentage2_complete_width)=%%%"&lt;/span&gt; &lt;span class="na"&gt;align=&lt;/span&gt;&lt;span class="s"&gt;"right"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        %%=v(@percentage2)=%%%
                        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"font-size: 6px; mso-line-height-rule: exactly; line-height: 6px;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="ni"&gt;&amp;amp;nbsp;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;

                    %%[if @percentage2_incomplete_width &amp;gt; 0 then]%%
                    &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"%%=v(@percentage2_incomplete_width)=%%%"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
                    %%[endif]%%
                &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"%%=v(@percentage2_complete_width)=%%%"&lt;/span&gt; &lt;span class="na"&gt;bgcolor=&lt;/span&gt;&lt;span class="s"&gt;"%%=v(@percentage2_color)=%%"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"background: %%=v(@percentage2_color)=%%;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"font-size: 18px; mso-line-height-rule: exactly; line-height: 18px;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="ni"&gt;&amp;amp;nbsp;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;

                    %%[if @percentage2_incomplete_width &amp;gt; 0 then]%%
                    &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"%%=v(@percentage2_incomplete_width)=%%%"&lt;/span&gt; &lt;span class="na"&gt;bgcolor=&lt;/span&gt;&lt;span class="s"&gt;"#bbbbbb"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"background: #bbbbbb;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"font-size: 18px; mso-line-height-rule: exactly; line-height: 18px;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="ni"&gt;&amp;amp;nbsp;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
                    %%[endif]%%
                &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Disclaimer: This is a loose proof of concept. It’s &lt;em&gt;far&lt;/em&gt; from elegant HTML, but it shows the rough idea.&lt;/p&gt;

&lt;p&gt;And here is our visual again of how it comes together:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feg78df6za3kmt1dl5xxo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feg78df6za3kmt1dl5xxo.png" alt="screenshot" width="630" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using this concept, there is a lot more you could do here to make it more elegant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Separate bars for goal vs. actual&lt;/li&gt;
&lt;li&gt;Numbers displayed inside the bars&lt;/li&gt;
&lt;li&gt;Repeating background images or possibly even an animated GIF to add texture/creative to the bars&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However you want your design to look, blending AMPscript with HTML/CSS allows dynamic colors and sizes of elements with nothing but code.&lt;/p&gt;




&lt;p&gt;I hope you found this helpful — if you have additional ways you’ve used AMPscript for percentages or dynamic email design, leave them in the comments!&lt;/p&gt;

</description>
      <category>sfmc</category>
      <category>ampscript</category>
    </item>
    <item>
      <title>AMPscript Has A 'Contains' Function, It's Just Not Obvious</title>
      <dc:creator>AMPscript Ninja</dc:creator>
      <pubDate>Fri, 17 Oct 2025 03:07:07 +0000</pubDate>
      <link>https://dev.to/ampscript-ninja/ampscript-has-a-contains-function-its-just-not-obvious-4m4i</link>
      <guid>https://dev.to/ampscript-ninja/ampscript-has-a-contains-function-its-just-not-obvious-4m4i</guid>
      <description>&lt;p&gt;Need to check if one string contains another? Probably constantly. It's surprising that with how many string comparison operators and functions AMPscript has, there isn't a &lt;code&gt;contains&lt;/code&gt; operator or a &lt;code&gt;Contains()&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;Not to worry, you can do it with the &lt;code&gt;IndexOf()&lt;/code&gt; function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Index Of What, Exactly?
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;IndexOf()&lt;/code&gt; function (&lt;a href="https://developer.salesforce.com/docs/marketing/marketing-cloud-ampscript/references/mc-ampscript-string/mc-ampscript-reference-string-index-of.html" rel="noopener noreferrer"&gt;SFMC documentation here&lt;/a&gt;) allows you to check where inside a larger string a smaller substring exists. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%%=IndexOf('I am an AMPscript ninja.','ninja')=%%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This would result in &lt;code&gt;19&lt;/code&gt;, because "ninja" starts at character 19 in the larger phrase "I am an AMPscript ninja."&lt;/p&gt;

&lt;p&gt;If the larger string doesn't contain the substring at all, the result is &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I don't know about you, but I've been AMPscripting for 8 years and never once had a use case where I needed to know where a character or set of characters occurred in a string. But I've been using &lt;code&gt;IndexOf()&lt;/code&gt; on a daily basis because...&lt;/p&gt;

&lt;h2&gt;
  
  
  Knowing WHERE Also Tells You IF
&lt;/h2&gt;

&lt;p&gt;Quite simply, knowing WHERE the substring is located might not be useful very often, but knowing IF the substring is there makes this work as a 'contains' function. If the result is not &lt;code&gt;0&lt;/code&gt;, then your larger string contains your smaller substring.&lt;/p&gt;

&lt;p&gt;Here's another example, this time using variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%%[
set @string = 'I am an AMPscript ninja.'
set @substring = 'ninja'

if IndexOf(@string,@substring) != 0 then
    set @string_contains_substring = true
endif
]%%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I see developers use &lt;code&gt;!= 0&lt;/code&gt; (is not equal to zero) and &lt;code&gt;&amp;gt; 0&lt;/code&gt; (is greater than zero) pretty equally, and either will work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Helpful Use Cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  If A Variable Contains A String
&lt;/h3&gt;

&lt;p&gt;I find myself using this most checking if a variable contains a string. Let's look at a variable field with various superhero names, and see which ones contain "Man".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%%[if IndexOf(@superhero_name,'Man') != 0 then]%%
    Hey, it's a classic superhero naming convention.
%%[else]%%
    At least it's less repetitive.
%%[endif]%%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Batman, Catwoman, Iron Man, Spider-Man, and many others would be in the first &lt;code&gt;if&lt;/code&gt; statement. Any others not containing "Man" or "man" would be in the &lt;code&gt;else&lt;/code&gt; statement.&lt;/p&gt;

&lt;h3&gt;
  
  
  If A Variable Is On A List
&lt;/h3&gt;

&lt;p&gt;Another way I like to use this function is to check if a variable's value is on a list of values. Instead of a long &lt;code&gt;if&lt;/code&gt; &lt;code&gt;elseif&lt;/code&gt; &lt;code&gt;elseif&lt;/code&gt; ... &lt;code&gt;endif&lt;/code&gt; pattern, we can create a single variable with a list of values, then check to see if our current variable is on that list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%%[
set @og_avengers = 'Iron Man,Captain America,Thor,Hulk,Hawkeye,Black Widow'
]%%

%%[if IndexOf(@og_avengers,@superhero_name) != 0 then]%%
    No roster will ever be the same.
%%[else]%%
    But Marvel/Disney will keep trying.
%%[endif]%%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  My Favorite: Easy Geo-Targeting
&lt;/h3&gt;

&lt;p&gt;For a more real-world example, this check-the-list concept gets very handy with geo-targeting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%%[
set @snowbelt_states = 'AK,CO,CT,DE,IA,ID,IL,IN,KS,KY,MA,MD,ME,MI,MN,MO,MT,NC,ND,NE,NH,NJ,NY,OH,OR,PA,RI,SD,TN,VA,VT,WA,WI,WV,WY'

if IndexOf(@snowbelt_states,@state) != 0 or @country == 'CA' then
    set @show_cold_weather_gear = true
else
    set @show_cold_weather_gear = false
endif
]%%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to get even narrower, pull all the postal codes in a radius around another postal code (&lt;a href="https://www.freemaptools.com/find-zip-codes-inside-radius.htm" rel="noopener noreferrer"&gt;my favorite map tool here&lt;/a&gt;), add those to a list, and show highly regionalized content for upcoming events.&lt;/p&gt;




&lt;p&gt;I hope you found this helpful - if you have a favorite way to use &lt;code&gt;IndexOf()&lt;/code&gt;, leave a comment!&lt;/p&gt;

</description>
      <category>sfmc</category>
      <category>ampscript</category>
    </item>
    <item>
      <title>3 Easy Geotargeting Options In SFMC (Including Postal Code Radius)</title>
      <dc:creator>AMPscript Ninja</dc:creator>
      <pubDate>Fri, 17 Oct 2025 03:03:19 +0000</pubDate>
      <link>https://dev.to/ampscript-ninja/3-ridiculously-easy-ways-to-geotarget-in-sfmc-4n5k</link>
      <guid>https://dev.to/ampscript-ninja/3-ridiculously-easy-ways-to-geotarget-in-sfmc-4n5k</guid>
      <description>&lt;p&gt;Need some easy options for geotargeting in Salesforce Marketing Cloud Engagement (ex-ExactTarget)? I have 3 super easy options for you to target by state or postal code.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;By state&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;By postal code radius&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;By &lt;strong&gt;a very large&lt;/strong&gt; postal code radius, or multiple&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And for each, I’ll give examples on how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Segment&lt;/strong&gt; your audience to control who &lt;strong&gt;receives&lt;/strong&gt; an email&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Personalize&lt;/strong&gt; your email to control who &lt;strong&gt;sees content in&lt;/strong&gt; an email&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  1) List Of States
&lt;/h1&gt;

&lt;p&gt;Okay, that sounds too easy, right? Anyone can list a bunch of rules in a filtered data extension or in Journey Builder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc9fhbq7znbix05gcyb5s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc9fhbq7znbix05gcyb5s.png" alt=" " width="800" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That works great. But here is our first tip, accomplishing it in a single line using the “exists in whole word” operator:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxjcbie3a1feqfmc14nym.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxjcbie3a1feqfmc14nym.png" alt=" " width="800" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This operator is great for checking if the entire value of your field (e.g. “MN”) is found on that list of values.&lt;/p&gt;

&lt;p&gt;Alright, let’s look at some AMPscript if you only want to show/hide content within an email going to a larger audience. Similar to a drag and drop filter, most devs start with the obvious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%%[
if @state == "MN" or @state == "WI" or @state == "IA" or @state == "ND" or @state == "SD" then
    set @show_midwest_content = true
endif
]%%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And again similarly, we can accomplish this with a list of values, using AMPscript’s &lt;code&gt;IndexOf()&lt;/code&gt; operator. This allows us to check if a smaller string is contained within a larger string, which conveniently works to check if a field’s value is contained within a list of values. (&lt;a href="https://ampscript-ninja.hashnode.dev/ampscript-has-a-contains-function-its-just-not-obvious" rel="noopener noreferrer"&gt;Learn more here&lt;/a&gt;.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%%[
/* A bit more literal */
set @midwest_states = "MN,WI,IA,ND,SD"

if IndexOf(@midwest_states,@state) != 0 then
    set @show_midwest_content = true
endif

/* A bit shorter */
if IndexOf("MN,WI,IA,ND,SD",@state) != 0 then
    set @show_midwest_content = true
endif
]%%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Either of these options is a bit shorter than listing out each state with an &lt;code&gt;or&lt;/code&gt; operator.&lt;/p&gt;




&lt;h1&gt;
  
  
  2) Postal Code Radius
&lt;/h1&gt;

&lt;p&gt;Spoiler: This is the same solution as states, but we’re going to take a quick detour to one of my favorite bookmarks, &lt;a href="https://www.freemaptools.com/find-zip-codes-inside-radius.htm" rel="noopener noreferrer"&gt;US&lt;/a&gt; or &lt;a href="https://www.freemaptools.com/find-canada-postcodes-inside-radius.htm" rel="noopener noreferrer"&gt;Canada&lt;/a&gt; postal code radius generator by freemaptools.com.&lt;/p&gt;

&lt;p&gt;To get started, select your radius in kilometers or miles, enter your starting postal code, and click &lt;strong&gt;Search.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgtnln38js33qfwgw5m4m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgtnln38js33qfwgw5m4m.png" alt=" " width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Scroll down to &lt;strong&gt;ZIP Codes&lt;/strong&gt; where you can see how many other postal codes are contained within that radius. They should be in a list separated by commas — click &lt;strong&gt;Copy To Clipboard&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa6tcpnedk93igzdojtjz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa6tcpnedk93igzdojtjz.png" alt=" " width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now head back to your filter in SFMCE and use the “exists in whole word” operator again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbfa1ugr491vdxh1ribhr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbfa1ugr491vdxh1ribhr.png" alt=" " width="800" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Quirky display aside, &lt;em&gt;yes, this actually does work&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;At least, within limits. I’ve done it without issues up to a couple hundred values, but I wouldn’t recommend over 500. (Salesforce support probably wouldn’t recommend it at all.) So if you need a &lt;em&gt;really&lt;/em&gt; large list, scroll on to #3 below.&lt;/p&gt;

&lt;p&gt;But first, a quick example of how the same &lt;code&gt;IndexOf()&lt;/code&gt; solution works here for using AMPscript to show/hide content based on the postal code radius:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%%[
set @postal_code_radius = "55442,55428,55446,55441,55427,55447,55429,55422,55569,55369,55445,55311,55430,55443,55305,55412,55426,55411,55340,55444,55391,55405,55416,55403,55356,55316,55432,55401,55421,55402,55479,55418,55345,55470,55343,55440,55458,55459,55480,55483,55484,55485,55486,55433,55487,55474,55488,55408,55323,55413,55415,55599,55436,55467,55404,55454,55361,55455,55424,55409,55410,55327,55414,55374,55592,55577,55593,55357,55407,55448,55419,55346,55112,55434,55439,55384,55406,55344,55435,55114,55359,55108,55113,55417,55423,55341,55364,55331,55449,55104,55126,55373,55317,55347,55105,55376,55450,55438,55103,55116,55572,55437,55111,55303,55304,55431,55117,55420,55127,55150,55014,55386,55155,55102,55425,55146,55130,55131,55133,55145,55164,55170,55120,55101,55301,55107,55118,55328,55375,55318,55121,55106,55109,55337,55378,55122,55330,55110,55379,55388,55387,55119,55123,55075,55144,55313,55011,55306,55090,55077,55128,55363,55038,55124,55115,55055,55076,55025,55125,55070,55372,55092,55360,55005,55322,55042,55362,55315,55071,55390,55068,55367,55129,55398,55082,55352,55309,55044,55016,55397,55040,55083,55047,55358,55349,55001,55079,55368,55003,55020,55395,55308,55078,55024,55073,55054,54082,55043,55339,55354,56011,55085,55013,55033,55370,56071,54016,55088,55008,55321,54025,55010,55302,55381,55338,55045,55371,55031,55320,55377,55319,55056,55065,55382,55046,55074,54021,55057,56044,55029,55012,54020,55336,54022,54023,55017,55032,55080,56069,55325,56304,55019,55084,55307,54017,54026,54009,56363,55353,55089,55009,55366,54015,56058,55324,55018,56057,56357,55006,56330,55069,54024,55334,56397,56398,54014"

if IndexOf(@postal_code_radius,@postal_code) != 0 then
    set @show_event_content = true
endif
]%%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  3) Bigger Postal Code Radius
&lt;/h1&gt;

&lt;p&gt;You need a really large postal code radius, or multiple radiuses? (Radii? Weird. Let’s stick with radiuses.) Or maybe you just don’t want to abuse/crash SFMCE with that big of a list of values.&lt;/p&gt;

&lt;p&gt;No problem, we have another great option.&lt;/p&gt;

&lt;p&gt;Starting back at freemaptools.com, search for your larger list of postal codes — but this time, click the &lt;strong&gt;Toggle CSV or New Line&lt;/strong&gt; button so your results are on individual lines before clicking &lt;strong&gt;Copy To Clipboard&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsajmmbm1p32qiwl42mge.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsajmmbm1p32qiwl42mge.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Paste that list into Notepad or a code editor following “postal_code” and save as a .txt file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;postal_code
55442
55428
55446
55441
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you’re working with multiple postal code radiuses, you can combine them by pasting multiple lists here into the same file.&lt;/p&gt;

&lt;p&gt;What we’re going to do is create a new data extension in SFMCE containing all of your postal codes, then create a data relationship between your main data extension (wherever your starting point is for an email audience) and the postal codes.&lt;/p&gt;

&lt;p&gt;Create a new &lt;strong&gt;Standard Data Extension&lt;/strong&gt; — on the &lt;strong&gt;(3) Fields&lt;/strong&gt; tab, add two fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;postal_code&lt;/strong&gt; (or whatever your postal code field is typically named), set as the &lt;strong&gt;Primary Key&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;in_radius&lt;/strong&gt; — a &lt;strong&gt;Boolean&lt;/strong&gt; field that is &lt;strong&gt;Nullable&lt;/strong&gt; and has a &lt;strong&gt;Default Value&lt;/strong&gt; of &lt;strong&gt;True&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzy1lrx74rlz4l2z8plh5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzy1lrx74rlz4l2z8plh5.png" alt=" " width="800" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you’ve created it, import your .txt file, and it should automatically populate that &lt;strong&gt;in_radius&lt;/strong&gt; field for you.&lt;/p&gt;

&lt;p&gt;Next, create a &lt;strong&gt;Data Relationship&lt;/strong&gt; between your starting data extension and the one you just created, joining on &lt;strong&gt;postal_code&lt;/strong&gt;. As long as your new data extension has &lt;strong&gt;postal_code&lt;/strong&gt; as the primary key (unique identifier) and your starting data extension also has &lt;strong&gt;postal_code&lt;/strong&gt;, you should see the all-important data filter checkbox checked, indicating that you can use this in a filter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmfrrmuaxsgwc04zjynoo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmfrrmuaxsgwc04zjynoo.png" alt=" " width="800" height="292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Head back to your filtered data extension, scroll down past the field names to &lt;strong&gt;Data Relationships&lt;/strong&gt;, and open the new one you just created. Drag over the &lt;strong&gt;in_radius&lt;/strong&gt; field and set to &lt;strong&gt;is true&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7rt67rxxtg5sy8r3tqih.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7rt67rxxtg5sy8r3tqih.png" alt=" " width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(I apologize for the gray boxes here, redacting some fields and other data relationships not relevant to this use case and not wanting to overshare my company’s data model.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you’re not familiar with data relationships, we just created a forced join between the two tables, so they will only be included in your filtered results if their &lt;strong&gt;postal_code&lt;/strong&gt; is in your new list of &lt;strong&gt;postal_code&lt;/strong&gt; values. (For SQL users, this is equivalent to an &lt;code&gt;INNER JOIN&lt;/code&gt; on the &lt;strong&gt;postal_code&lt;/strong&gt; field.)&lt;/p&gt;

&lt;p&gt;Now you’re all set, with an audience filtered to only subscribers in a very large postal code radius (or one of multiple).&lt;/p&gt;

&lt;p&gt;For showing/hiding content in an email, AMPscript can easily target this same data extension with &lt;code&gt;Lookup()&lt;/code&gt; to check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%%[
set @postal_code = AttributeValue("postal_code")

/* Connect to the "postal_code_radius" DE to get "in_radius", matching on "postal_code". */
set @in_radius = Lookup("postal_code_radius","in_radius","postal_code",@postal_code)

if @in_radius == true then
    set @show_event_content = true
endif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;And there you have it — three easy copy-and-paste-friendly methods for geotargeting in SFMCE. Share any easy tips I missed in the comments, and thanks for reading!&lt;/p&gt;

</description>
      <category>sfmc</category>
      <category>ampscript</category>
    </item>
  </channel>
</rss>
