<?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>How To Change The Entry Event Data Extension In A Live SFMC Journey</title>
      <dc:creator>AMPscript Ninja</dc:creator>
      <pubDate>Fri, 29 May 2026 02:12:52 +0000</pubDate>
      <link>https://dev.to/ampscript-ninja/how-to-change-the-entry-event-data-extension-in-a-live-sfmc-journey-2732</link>
      <guid>https://dev.to/ampscript-ninja/how-to-change-the-entry-event-data-extension-in-a-live-sfmc-journey-2732</guid>
      <description>&lt;p&gt;Automated email programs are always evolving as your business and customers change – and inevitably, your data will, too.&lt;/p&gt;

&lt;p&gt;Following are steps for two scenarios to update the data extension in the entry event for a live journey in Salesforce Marketing Cloud Engagement (MCE), without needing to create a new version of the journey.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Adding new fields to the existing data extension&lt;/li&gt;
&lt;li&gt; Changing to a new data extension &lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Adding New Fields To The Existing Data Extension
&lt;/h2&gt;

&lt;p&gt;This is really easy. Once you've added your new field – and presumably, your email(s) to reference the new field – all you'll need to do is publish changes to the journey.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Start in &lt;strong&gt;Email Studio&lt;/strong&gt; &amp;gt; &lt;strong&gt;Interactions&lt;/strong&gt; &amp;gt; &lt;strong&gt;Triggered Sends&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  Click on the correct journey name and version in the left side navigation, which will open you a list of triggered sends – i.e. all the emails in your journey&lt;/li&gt;
&lt;li&gt;  Select all and click &lt;strong&gt;Pause&lt;/strong&gt;, and wait for the green confirmation banner&lt;/li&gt;
&lt;li&gt;  Select all and click &lt;strong&gt;Publish Changes&lt;/strong&gt;, and wait for the green confirmation banner&lt;/li&gt;
&lt;li&gt;  Select all and click &lt;strong&gt;Start/Restart&lt;/strong&gt;, and wait for the green confirmation banner&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it... your email changes and DE changes are now active, and your email now has access to the new field. (But of course – test it to be sure!)&lt;/p&gt;




&lt;h2&gt;
  
  
  Changing To A New Data Extension
&lt;/h2&gt;

&lt;p&gt;This is a bit more drastic of a change, and there are probably very few real-world scenarios where you'd change the data extension without also changing the journey, which would have dependencies for its &lt;strong&gt;Decision Split&lt;/strong&gt; activities, etc. However, it can be done.&lt;/p&gt;

&lt;p&gt;(Personally, I've only done this with staged/in-progress journeys where the data extension is still going through refinement. For example, if I identified a field to add that we missed earlier, and I want it earlier/higher in the data extension adjacent to similar fields, not by itself at the end as the last field. In that case, I may create a new version of the DE with the fields in the order I want, and change the journey entry event to use the new DE. Field order probably isn't that important to most people... maybe I'm just OCD.)&lt;/p&gt;

&lt;p&gt;To change the DE, you'll need to edit the journey's entry event, and to do that, you'll need to pause every journey using that entry event. If you go to &lt;strong&gt;Journey Builder&lt;/strong&gt; &amp;gt; &lt;strong&gt;Events&lt;/strong&gt; &amp;gt; &lt;strong&gt;Entry Sources&lt;/strong&gt; and look at your entry event, you'll see what appears to be a link saying how many journeys use that event. Frustratingly, clicking it will simply take you to the full journey list on the JB home page, with no filters or indicators showing which journeys. So unfortunately, you'll need to figure that part out on your own.&lt;/p&gt;

&lt;p&gt;Once you've identified which journey(s) to update:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Open the journey &amp;gt; click the blue &lt;strong&gt;Pause&lt;/strong&gt; button in the top right &amp;gt; &lt;strong&gt;Pause This Version&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  Go through the &lt;strong&gt;Pause Options&lt;/strong&gt;, which is deciding whether you want the journey to auto-resume after a period of time. What you select here is effectively irrelevant since you'll manually resume in a few minutes.&lt;/li&gt;
&lt;li&gt;  Click &lt;strong&gt;OK&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  Keep the window open and don't click or reload anything as you wait [somewhat] patiently for your mouse cursor to change from a spinning blue circle to the standard cursor. You'll know it's done when the blue button in the top right now says &lt;strong&gt;Resume&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;  Close the journey, and head back to &lt;strong&gt;Events&lt;/strong&gt; &amp;gt; &lt;strong&gt;Entry Sources&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  Open your entry event, which will probably jump you to step 4, &lt;strong&gt;SUMMARY&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  Click on step 2, &lt;strong&gt;SELECT DE&lt;/strong&gt;, and select your new DE

&lt;ul&gt;
&lt;li&gt;  (If you don't see your DE in this list, go back to it in Email Studio and make sure you have it marked as &lt;strong&gt;Sendable&lt;/strong&gt; with a mapping to subscriber key)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;  Click &lt;strong&gt;Next&lt;/strong&gt; through steps 3 and 4, then &lt;strong&gt;Done&lt;/strong&gt;
&lt;/li&gt;

&lt;li&gt;  Go back to your journey(s) &amp;gt; &lt;strong&gt;Resume&lt;/strong&gt; &amp;gt; &lt;strong&gt;Resume This Version&lt;/strong&gt;
&lt;/li&gt;

&lt;li&gt;  Once again, keep the window open and don't click or reload anything as you wait for your mouse cursor to stop spinning. You'll know it's done when the blue button in the top right has gone back to saying &lt;strong&gt;Pause&lt;/strong&gt;.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;And there you go, your journey is now using your new data extension.&lt;/p&gt;

&lt;p&gt;A couple of additional considerations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  You'll obviously want to also make any changes to the email(s) at this time, referencing any new fields and not referencing any removed fields.&lt;/li&gt;
&lt;li&gt;  In my experience, any fields that are named the same in both the old and new DEs, any &lt;strong&gt;Decision Split&lt;/strong&gt; activities in the journey will recognize the field and still work. However, be sure to test this for yourself. For any removed/renamed fields referenced by splits, those splits will fail and default to the fallback path – so if you're changing fields used by journey activities, you're better off creating a new version of the journey.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Have another question about Journey Builder? Drop it in the comments!&lt;/p&gt;

</description>
      <category>sfmc</category>
    </item>
    <item>
      <title>How To Limit The Number Of Words Or Characters Shown With SFMC's AMPscript</title>
      <dc:creator>AMPscript Ninja</dc:creator>
      <pubDate>Wed, 06 May 2026 19:19:13 +0000</pubDate>
      <link>https://dev.to/ampscript-ninja/how-to-limit-the-number-of-words-or-characters-shown-with-sfmcs-ampscript-3jk2</link>
      <guid>https://dev.to/ampscript-ninja/how-to-limit-the-number-of-words-or-characters-shown-with-sfmcs-ampscript-3jk2</guid>
      <description>&lt;p&gt;Ever had a scenario where you were putting a product name or some other kind of dynamic content into a subject line, and you wanted to only show the first X words or characters? Or featuring article content in an email and wanted to include a short blurb with the first X words as a preview?&lt;/p&gt;

&lt;p&gt;Let's look at how to limit characters (easy with limitations) and whole words (more involved but better control and end user experience).&lt;/p&gt;

&lt;h2&gt;
  
  
  By Characters: &lt;code&gt;Substring()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The first and easiest solution is to use the &lt;code&gt;Substring()&lt;/code&gt; function, which allows you to designate a starting position and a number of characters to show. For example, this would create a new &lt;code&gt;@preview_text&lt;/code&gt; variable with the first (starting from position 1) 100 characters in a longer &lt;code&gt;@full_length_text&lt;/code&gt; variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set @preview_text = Substring(@full_length_text,1,100)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a really powerful function when you know exactly what character position you want to start and how many characters you want. However, it can lead to unexpected results because you have limited control over where things break.&lt;/p&gt;

&lt;p&gt;As a real-world example, here is how I set up the subject line for a back-in-stock alert email, adding the first 30 characters of the product name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set @subject_line = Concat('Back In Stock: ',Substring(@product_name,1,30),'...')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple enough — a product name of "Totally Fake Awesome Product Name" will appear as:&lt;br&gt;
&lt;strong&gt;Back In Stock: Totally Fake Awesome Product N...&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But you don't know where that will break, including in the middle of a word. One day while testing an update and reviewing emails sent to real customers, I did a double-take on this one:&lt;br&gt;
&lt;strong&gt;Back In Stock: Polaris Full Coverage Rear Bum...&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not the greatest place for the text to cut — suddenly a protective bumper for an off-road vehicle (&lt;a href="https://www.polaris.com/en-us/shop/off-road/accessories/bumpers-guards/ranger-bumpers-guards/2884217/" rel="noopener noreferrer"&gt;Polaris Full Coverage Rear Bumper&lt;/a&gt;) sounds a little... inappropriate. That was when I knew I had to switch to whole words.&lt;/p&gt;


&lt;h2&gt;
  
  
  By Words: &lt;code&gt;BuildRowsetFromString()&lt;/code&gt; And A &lt;code&gt;for&lt;/code&gt; Loop
&lt;/h2&gt;

&lt;p&gt;This is a bit more involved but insures that everything is checked in whole words. We'll do two versions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Allowing X words, regardless of character length&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Allowing whole words up to X total characters&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I originally applied this to my subject line example above, but let's use some classic Lorem Ipsum text to show it on a larger scale (for example, if you were including an article preview in an email).&lt;/p&gt;
&lt;h3&gt;
  
  
  Allowing X Words
&lt;/h3&gt;

&lt;p&gt;The first thing we'll do is break down our full-length text into individual words with the &lt;code&gt;BuildRowsetFromString()&lt;/code&gt; function, splitting on spaces.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set @full_length_text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'

set @words_rowset = BuildRowsetFromString(@full_length_text," ")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This breaks a single string of text...&lt;br&gt;
&lt;strong&gt;Lorem ipsum dolor sit amet...&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;...into a rowset, where each word is now a single field in its own row.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lorem
ipsum
dolor
sit
amet
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, we'll determine how many words we want to include.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set @allowed_words = 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Third, we'll start a &lt;code&gt;for&lt;/code&gt; loop to iterate through the first X words (based on &lt;code&gt;@allowed_words&lt;/code&gt;) and &lt;code&gt;Concat()&lt;/code&gt; them back together with spaces.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/* Set an empty variable that will be filled by the 'for' loop. */
set @short_text = ''

/* Start a 'for' loop, which iterates (@i) from the 1st word to the number of allowed words, and 'do' steps for each. */
for @i = 1 to @allowed_words do

    /* Lock into each row. */
    set @i_words_row = Row(@words_rowset,@i)

    /* Get the 1st (and only) field in each row, which is a single word. */
    set @i_word = Field(@i_words_row,1)

    /* Add the current word to the short text variable, including a space. */
    set @short_text = Concat(@short_text,@i_word,' ')
next @i
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, we would have...&lt;br&gt;
&lt;strong&gt;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;...20 words with a space after each, including after the last word.&lt;/p&gt;

&lt;p&gt;Let's do some quick cleanup, starting with removing the trailing space with the &lt;code&gt;Trim()&lt;/code&gt; function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/* Remove any trailing spaces. */
set @short_text = Trim(@short_text)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another thing I like to avoid is punctuation at the end. Since we're defining words as separated by spaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A word followed by a comma includes the comma in the word, and we don't want ",..." at the end.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Standalone punctuation commonly separated by spaces would also be treated as words, including ampersands &lt;code&gt;&amp;amp;&lt;/code&gt; and all forms of hyphens and dashes.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To clean that up, we'll use &lt;code&gt;Substring()&lt;/code&gt; to select the &lt;strong&gt;last&lt;/strong&gt; character of the text, and compare it to a list of punctuation marks with &lt;code&gt;IndexOf()&lt;/code&gt; (&lt;a href="https://dev.to/ampscript-ninja/ampscript-has-a-contains-function-its-just-not-obvious-4m4i"&gt;AMPscript's best "contains" function&lt;/a&gt;). If it matches, then we'll use &lt;code&gt;Substring()&lt;/code&gt; one more time to trim/remove the last character.&lt;/p&gt;

&lt;p&gt;(If you wanted, you could use the &lt;code&gt;RegExMatch()&lt;/code&gt; function to get really advanced on this check, but a simple list of punctuation has been sufficient in my experience.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/* Select the last character, and if it's punctuation, remove it. */
set @last_character = Substring(@short_text,Length(@short_text),1)

if IndexOf('-–—,;:&amp;amp;+/\',@last_character) != 0 then
    set @short_text = Substring(@short_text,1,Subtract(Length(@short_text),1))
endif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we'll add an ellipsis &lt;code&gt;...&lt;/code&gt; if the total word count is greater than the allowed word count.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/* If the original phrase had more words than allowed, then add an ellipsis at the end. */
if RowCount(@words_rowset) &amp;gt; @allowed_words then
    set @short_text = Concat(@short_text,'...')
endif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our original text has significantly more than 20 words, so with cleanup, it displays as:&lt;br&gt;
&lt;strong&gt;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut...&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the text had fewer words than allowed, no trimming would be done and the entire thing would be displayed.&lt;/p&gt;

&lt;p&gt;Here is the complete ready-to-copy/paste code — just set your own value for &lt;code&gt;@full_length_text&lt;/code&gt; and determine the &lt;code&gt;@allowed_words&lt;/code&gt; count.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set @full_length_text = 'Any text or variable name you want'

/* Break down the full text into individual words split by spaces. */
set @words_rowset = BuildRowsetFromString(@full_length_text," ")

/* Decide how many words should be included. */
set @allowed_words = 20

/* Set an empty variable that will be filled by the 'for' loop. */
set @short_text = ''

/* Start a 'for' loop, which iterates (@i) from the 1st word to the number of allowed words, and 'do' steps for each. */
for @i = 1 to @allowed_words do

    /* Lock into each row. */
    set @i_words_row = Row(@words_rowset,@i)

    /* Get the 1st (and only) field in each row, which is a single word. */
    set @i_word = Field(@i_words_row,1)

    /* Add the current word to the short text variable, including a space. */
    set @short_text = Concat(@short_text,@i_word,' ')
next @i

/* Remove any trailing spaces. */
set @short_text = Trim(@short_text)

/* Select the last character, and if it's punctuation, remove it. */
set @last_character = Substring(@short_text,Length(@short_text),1)

if IndexOf('-–—,;:&amp;amp;+/\',@last_character) != 0 then
    set @short_text = Substring(@short_text,1,Subtract(Length(@short_text),1))
endif

/* If the original phrase had more words than allowed, then add an ellipsis at the end. */
if RowCount(@words_rowset) &amp;gt; @allowed_words then
    set @short_text = Concat(@short_text,'...')
endif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Allowing Words Up To X Characters
&lt;/h3&gt;

&lt;p&gt;Obviously not all words are the same length, so this can still vary quite a bit with longer vs. shorter words. Let's look at another variant that doesn't allow a specific number of words, but allows words until a specific number of total characters is reached.&lt;/p&gt;

&lt;p&gt;There are some key differences here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Instead of setting a count of &lt;code&gt;@allowed_words&lt;/code&gt;, it sets &lt;code&gt;@allowed_characters&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When starting the &lt;code&gt;for&lt;/code&gt; loop, it iterates through all words, using &lt;code&gt;RowCount(@words_rowset)&lt;/code&gt; instead of &lt;code&gt;@allowed_words&lt;/code&gt; to control the number of iterations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Within the &lt;code&gt;for&lt;/code&gt; loop, there is an additional &lt;code&gt;if&lt;/code&gt; check comparing the word's character count and the character count so far, to see if adding the next word will exceed the max character count.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The ellipsis check looks at the character count of the original full-length text instead of the number of words.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set @full_length_text = 'Any text or variable name you want'

/* Break down the full text into individual words split by spaces. */
set @words_rowset = BuildRowsetFromString(@full_length_text," ")

/* Decide how many characters should be allowed. */
set @allowed_characters = 150

/* Set an empty variable that will be filled by the 'for' loop. */
set @short_text = ''

/* Start a 'for' loop, which iterates (@i) from the 1st word to the number of total words, and 'do' steps for each. */
for @i = 1 to RowCount(@words_rowset) do

    /* Lock into each row. */
    set @i_words_row = Row(@words_rowset,@i)

    /* Get the 1st (and only) field in each row, which is a single word. */
    set @i_word = Field(@i_words_row,1)

    /* If adding the current word to the short text (so far) will not exceed the allowed number of characters, then add it. */
    if Add(Length(@i_word),Length(@short_text)) &amp;lt; @allowed_characters then

        /* Add the current word to the short text variable, including a space. */
        set @short_text = Concat(@short_text,@i_word,' ')

    endif

next @i

/* Remove any trailing spaces. */
set @short_text = Trim(@short_text)

/* Select the last character, and if it's punctuation, remove it. */
set @last_character = Substring(@short_text,Length(@short_text),1)

if IndexOf('-–—,;:&amp;amp;+/\',@last_character) != 0 then
    set @short_text = Substring(@short_text,1,Subtract(Length(@short_text),1))
endif

/* If the original phrase had more characters than allowed, then add an ellipsis at the end. */
if Length(@full_length_text) &amp;gt; @allowed_characters then
    set @short_text = Concat(@short_text,'...')
endif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Going back to our Lorem Ipsum example, when set to allow up to 150 characters, it cuts after "...veniam,".&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The existing &lt;code&gt;@short_text&lt;/code&gt; up to that point is at 149 characters, including the last space.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Adding 4 more characters for "quis" would be a total of 153, exceeding the 150 character limit, so "quis" and all following words are skipped.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Our punctuation check trims off the comma.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And finally we see: &lt;strong&gt;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam...&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you have additional use cases or limitations not covered here — or want to share your own examples of trimmed text gone wrong — drop me a comment!&lt;/p&gt;

</description>
      <category>sfmc</category>
      <category>ampscript</category>
    </item>
    <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>
