<?xml version= "1.0" encoding= "utf-8" standalone= "yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Salesforce on Spinning Code</title>
    <link>https://spinningcode.org/tags/salesforce/</link>
    <description>Recent content in Salesforce on Spinning Code</description> <generator>Hugo -- 0.157.0</generator>
    <language>en-US</language> <lastBuildDate>Wed, 30 Sep 2026 00:00:00 +0000</lastBuildDate> <atom:link href= "https://spinningcode.org/tags/salesforce/feed.xml" rel= "self" type= "application/rss+xml" /> <item>
      <title>Speak the Language of Your Audience</title>
      <link>https://spinningcode.org/2025/speak-audience-language/</link>
      <pubDate>
        Wed, 30 Sep 2026 00:00:00 +0000
      </pubDate> <guid isPermaLink="false">https://spinningcode.org/2025/speak-audience-language/</guid>  <description>To do our best work, we need to understand the problem we&amp;#39;re solving</description> <content:encoded><![CDATA[<p>I&rsquo;ve worked in several settings where technical people announced their focus was on the technology, not the user of the technology.
They would say things like: &ldquo;I don&rsquo;t care what they do, I just want to build it.&rdquo;</p>
<p>This is a terrible way to work.
It leads to bad solutions and miscommunication.</p>
<p>You cannot find the flaws in the spec, and there are always flaws, if you cannot sanity check it yourself.
You cannot know for sure your solution worked if you don&rsquo;t know what the data means and how it is used.</p>
<h2 id="the-importance-of-understanding">The Importance of Understanding</h2>
<p>When my wife and I attended the Polo match pictured above we tried to ask some of the regulars basic questions about what was happening.
They responded to us using all the words of the sport, but not in a way we could understand.
If they wanted to attract new fans to the sport, they needed to explain the game to us in words we understood.
It got even worse when a horse got injured and no one really explained what was happening until the ambulance trailer pulled onto the field.</p>
<p>To do our best work we need to understand the end goal of our users.
The most effective way I know to <a href="/tags/writing/">communicate clearly</a> is to use the same language as my audience does when talking about their work.
If you try to force them to use the terms and phrases you know best they will likely make mistakes that cause you problems.</p>
<p>We must meet our audiences where they are.</p>
<p>When I was a Drupal developer, I was creating web sites that were customized to a specific organization&rsquo;s needs.
When I was a Salesforce consultant and developer, I was tailoring the platform to handle the specific needs of those users.
In my role inside a nonprofit energy company I am making sure our Salesforce org meets our needs.</p>
<p>What was common across all those roles is that I needed (and still need) to talk with my users and understand their needs and the work they need the tool to do for them.</p>
<h3 id="problems-with-failure-to-understand">Problems with Failure to Understand</h3>
<p>When I was a web developer, often the spec I was working to support was a graphic design.
The designers would create a very attractive design, the client would approve it, and I was told to go implement it.
Drupal, of course, can be implemented many different ways.
To create a good implementation I needed to understand the designer&rsquo;s intent and why the client liked the design (beyond it being pretty).
I would ask why images were placed where they were, not to challenge the design but to make sure I called them the right thing in the backend.
Those names were important to the content editors to help them select the right image in the future.</p>
<p>Working with Salesforce brings the same challenges.
To understand if a Flow is working right, or even worth building at all, I need to know what the user expects to happen and <em>why</em>.
When organizing a page layout, I need to know which information is most important and what fields are related to group them sensibly.</p>
<h3 id="a-quick-note-about-ethics">A Quick Note About Ethics</h3>
<p>A complete discussion of the <a href="https://www.scu.edu/ethics/focus-areas/technology-ethics/">ethics of technology</a> is impossible here; it&rsquo;s a whole field of thought within Philosophy.
Still we all should care how our work impacts the world.
If I create a tools that&rsquo;s helping someone commit evil, I&rsquo;m supporting that evil.
There is a mountain of nuance and detail to be thought through.</p>
<p>Just because the topic is huge and complicated does not free us from the basic responsibility to care about it.
When someone tells me they don&rsquo;t care how the technology is used, they are implicitly announcing they are comfortable supporting outcomes they consider evil.</p>
<h2 id="when-do-i-know-enough">When Do I Know Enough?</h2>
<p>Probably never.</p>
<p>Over the course of my career so far I have worked on projects that involved:</p>
<ul>
<li>International peace and conflict resolution efforts</li>
<li>The US federal budget</li>
<li>Private prisons</li>
<li>Economic empowerment of women in Africa</li>
<li>Lobbying at the federal and state levels</li>
<li>The DNS system</li>
<li>Youth empowerment in the US and other parts of the world</li>
<li>Explaining Quakerism</li>
<li>Phone system setup and operation</li>
<li>Desktop computer and server hardware</li>
<li>Web development</li>
<li>Web site infrastructure</li>
<li>Virtual machines</li>
<li>Open source licenses</li>
<li>Copyright (copyleft) and trademarks</li>
<li>General nonprofit fundraising techniques and strategies</li>
<li>Basic accounting practices</li>
<li>The child welfare system in several US states</li>
<li>Life guarding and water safety</li>
<li>Natural disaster planning and response</li>
<li>Rules for credit card processing and security</li>
<li>Sales of kayaks, canoes, and other related products</li>
<li>Advanced and basic marketing strategies</li>
<li>Chemical coatings for packaging</li>
<li>The US sales tax &ldquo;system&rdquo;</li>
<li>The energy industry in California and generally around the US</li>
<li>&hellip;and probably a bunch of other stuff I&rsquo;m not thinking to include</li>
</ul>
<p>While that might be a little more than is average in just over 25 years of professional life it&rsquo;s probably not all <em>that</em> high for someone with a similar career track.
If you&rsquo;ve been working for a few years, think about all the different things you have had to work on – it&rsquo;s a bigger list than you&rsquo;ll expect.</p>
<p>All of that learning can be exhausting; I understand the temptation to say &ldquo;I know enough, I don&rsquo;t need to know more.&rdquo;
But if I want to keep getting chances to advance, I need to be ready to understand the next thing I need to know.</p>
<p>It helps to <a href="/2025/thoughts-on-professional-development/#be-curious">be curious</a>.</p>
]]></content:encoded> </item> <item>
      <title>Last Call: Salesforce Security Updates</title>
      <link>https://spinningcode.org/2026/salesforce-security-updates/</link>
      <pubDate>
        Mon, 29 Jun 2026 00:00:00 +0000
      </pubDate> <guid isPermaLink="false">https://spinningcode.org/2026/salesforce-security-updates/</guid>  <description>Last call to prepare for Salesforce security changes for July, phishing resistant MFA and more.</description> <content:encoded><![CDATA[<p>Over the next few weeks Salesforce will be <a href="https://help.salesforce.com/s/articleView?id=005317465&amp;type=1">rolling out security updates for all customers</a>.
If you haven&rsquo;t noticed or been preparing this is your last call!</p>
<h2 id="whats-happening">What&rsquo;s Happening</h2>
<p>A lot of people have been talking about these updates across the community, so hopefully this isn&rsquo;t news.
Here&rsquo;s a quick list of critical changes coming soon (the whole list is longer, see article above):</p>
<ul>
<li><a href="https://help.salesforce.com/s/articleView?id=005321563&amp;type=1&amp;language=en_US">Phishing-Resistant MFA Enforcement for Privileged Users, Including Admins</a>: Production release starting July 1, 2026, staggered over approximately 30 days.</li>
<li><a href="https://help.salesforce.com/s/articleView?id=005321561&amp;type=1&amp;language=en_US">MFA Enforcement for All Employee Users</a>: Production release starting July 20, 2026 staggered over approximately 30 days</li>
<li><a href="https://help.salesforce.com/s/articleView?language=en_US&amp;id=005321566&amp;type=1">Step-up Auth for Report Activities</a>: Production release starting July 1, 2026, staggered over approximately 30 days</li>
</ul>
<p>The biggest, most important one to track is Phishing Resistant MFA (PRMFA) Enforcement for Privileged Users.
If you are a Salesforce admin, and do not prepare for this update,<em>you risk getting locked out of the org.</em></p>
<h2 id="whats-the-lowest-bar-to-keep-things-running">What&rsquo;s the lowest bar to keep things running?</h2>
<p>All admins, even those who use SSO to login, should setup PRMFA inside of Salesforce.
If everything else fails, at least you&rsquo;ll still be able to get in.</p>
<p>Salesforce recommends <a href="help.salesforce.com/s/articleView?id=xcloud.identity_passkey_login_enable.htm&amp;type=5">setting up passkey-based authentication</a>.</p>
<ol>
<li>Turn on support for Phishing Resistant MFA:
<ol>
<li>In your org go to Setup.</li>
<li>Use the Quick Find box to find and select <code>Identity Verification</code>.</li>
<li>Select <code>Let users verify their identity with a built-in authenticator such as Touch ID or Windows Hello</code> and <code>Let users verify their identity with a physical security key (U2F or WebAuthn)</code></li>
<li>Save the change.






<figure class="figure-right">
  
  <a href="/images/posts/salesforce-security-updates-2026/personalSettings.png" target="_blank" rel="noopener noreferrer">
    
    

    











<noscript>
  <img class="rcf-image" src="/images/posts/salesforce-security-updates-2026/personalSettings.png" alt="Personal settings are under the menu item with a label of View Profile, then there is a settings link near the top. Image is a screenshot showing this." loading="lazy" />
</noscript>

<img class="rcf-image lazyload show-if-js"
     data-srcset="/images/posts/salesforce-security-updates-2026/personalSettings_hu_7f485054d2294c30.png 680w, /images/posts/salesforce-security-updates-2026/personalSettings.png 740w"
     data-src="/images/posts/salesforce-security-updates-2026/personalSettings.png"
     src="data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAAB8CAIAAAAuDC/&#43;AAAjHElEQVR4nOx9Z5cUV5bg82HTlRUgHaEe9fT2zs7u//8H&#43;2E/zDk9u9NGDQgKEFCVLjLcs3veu5FRUVkGgUAUUFc6UmVGZJh737veMK01uoNPB8wY86mf4asG5pz71M/wxYLbAkIIb2HnHMYY&#43;0SP9yWDtdYYo5SWUqrA5DljQgjOGaWUENKfyYYffiPsbKbhxyuJ/6WCMVZKuVoX8/lquS7qukEIJUk8HY/29iaT8SiKYko7tLPfjhc3gP6jCQDfEEIopYx5Yl9J7/7n1&#43;3TK38Cf7z15P6p4Mwbzu95xc1v2p9z5ZnGmLquX756/fjn589OXp3O13UjPQFicbA3/u7b4x&#43;&#43;f3Dv&#43;ChNE0qpJ8BbX/WtrwfbrX84hJDWum1bpVSPUEII5zyKIs75kAbwcyll27bWWtbtU3/OdYgA6mqtnXM9XW94vMAKlDGGECKEoJRevnL/FgghYBFXnqO1ZynGGHjOy5dyzkkpX705/a&#43;/PfrHo5M3Z0VRtq30LEhwuliVy/WmaVpCyIN7x3EcY4x/EwH6lQ5PDxgEdFhrOwnDmLVWKaW1ttZijDnn8NzwSkVRnJ6eLhYLrXWSJLPZbH9/P8syWCCX76iUKopitVpJKfM8n06nSZJcd7LWerPZLBaLuq7jOJ7NZqPRiLEL&#43;x5WQFVVZVlijPM8T9N0h66A2eVy&#43;ebNm7qu8zw/Ojoaj8c7lzLGFJvy6bOXPz1&#43;fjovMRFRRBxWCKFIcETo2aIiT16M82w8yjnnv1UC2y1orcuyrKoqTdM8z2ErMMaiKKKUAt5lAHgxWDvW2s1m8/Tp059&#43;&#43;mk&#43;nzvnhBBHR0c//vjjgwcP0jTdWdqA0NVqdXJy8vz586Zp9vb2vv/&#43;&#43;2&#43;&#43;&#43;SZJksv7ALC2WCxevny52WyEEFprznmWZT3WrLVVVc3n89evX8/nc4zx4eHh0dHRdDqN47i/Jjzqzz///Ne//nW9Xh8cHPz5z3/&#43;4Ycf8jzvLwXLcblan7x49fp0bRHdm44IoW3rWVAUCYTcYrF8c7Z&#43;9uLVg/vH4/HI7&#43;D3xj7wHFjdq9Xq&#43;fPn8/l8Mpk8ePAAaIAxNsa0bSulBKTDyZxzSilgc7FYPH78&#43;MmTJ1rrOI7X63Vd10mSTCaT4fv3d2ya5s2bNycnJ6enp8AQhBBpmgohrhQe8BhRFLUBNpuNlDJN0/4EY8xms3n27NnJyUlRFISQoii01iJA/wBw2snJyaNHj5bL5Xq9ns1mR0dHO6tE&#43;Q1dns5Xm6pN0nw8yqbjvG5ah1CWxJuqXq4Kf8LZal1stNKREL9VBgBOz87OHj9&#43;/OrVq9lsBmyHc26tbZpmsVgURZFl2d7eHrAjEBgYY&#43;A/8/lcSpllWZ7nZVk2TbNcLuu6Bia2c6&#43;6rlerVV3XjDGg0GazKctyOp1e3s0YYyHEdDoFsWSt3VEBe55W1zWsEoRQHWBon8I6q&#43;vaIy88YVmW6/W6aRpjTC8Jwg6wTSubRmptGCWTUXbveK9ppXMuz5LT&#43;frkxWutTd20TdMCHn4rAUDAwmLXWhtj4JteSYCV3ishw&#43;8v2yn9C19pHsKllPIslXMuhOi3YE/UnWdjjCVJUpalcw42VhRFVwptWDfOuStZmVKqbT3KOOdJkjDGYHPv&#43;BEwvF14F0ZplsXHRzMeVgbGSCrNg8wYvuwHIABjbDQa3bt3L0mSPM8nkwksf09exqbTaRSAEGKtHSoPjLEsy8bj8dnZWVVVwLKjKBqNRqAhXHk7&#43;B4ET0&#43;n6&#43;z5fjUAp5rNZlEUDa8Mzx/HcRRFsAmSJLmS&#43;zHG9vf327ZtmgaEPzDVoYJLKU3iKEsizqj1a8MmcXSwN8EEF0VlrbPOck6zNE6SGAT4&#43;xMA3g1gNBo9fPhQSgkijhCilIInA44P7wDK6JAAs9ns4cOHWuvlcokxTpLk4ODg/v37eZ5fVmwIIXEcT6fTpmnatoUXGI/HeZ7vaCM7BOOc53k&#43;Ho8vy2pKaZ7n3377bRRFy&#43;WSELK3t3d4eAhv0V9ECHF4eMgYOzo6atsWOOpsNgPZ01&#43;NMTYe50eHs1enK63Uar0pq2Z/NsYOl3WzXBVKylEWHR3OJuMR8MzftAOA5rC7gSHAC1trgQZKqR6PlNIoioaSjVI6Ho9/&#43;OGHNE1BDY3jeH9///DwME3TywglhKRpev/&#43;/fF4rJQC2sOO2UHEzq&#43;yLAOV7DKdMMagdGZZttlsgKJZlg3tFSDAbDbL8/zg4ABEdBzHw8W03Ux0Nhl//929&#43;aJ48WpxNl&#43;evHjFGaGUnLx48&#43;b0jBJ7fLT3/Xf3p9MxY/63&#43;Dc644ALD7k2fAPyYMg0KKWg&#43;Q5RAGwd9jWYEXEcX2cu9aoe6L5wQq/U3mC4wflAsOsuqwMAR7rh7j3Puc5Yk1Kdnp797acnf//n03VRjUf54cGMUjJfrFfrIkujP/7w3b/&#43;&#43;PDgYF8I/gEI0N945&#43;Ply17nZhhK4&#43;te7Ibb/UpXymURfd01f6NvxlrbtnI&#43;Xzw9efHs&#43;cuiKAPdKSE4z9L7946&#43;fXBvb28abTnBhyHAHQwBTOv1upjPF6tio7UOwjkej/PJJPC3gZl9R4APDL0vErwvgRV77scF92ZXQP0FNewyAe5I8ltg6Nfr3WIggYZuvnPRPUR37124o8H7wdD13cv83mM2tFt7SrDhj/tTr7NF7&#43;DXABlAvw&#43;AL8GGIISAVUQIuYIA8JuvJ4D1AaEPQA1DT0PfzA6PObeEe&#43;yDbXWdvnjxizsa7cJlvxZAz5F6Ea21tc4iTK7wIF5e/tY6Y63R1gTa4c78IawTKnd06OAGVMDyB9oY65QxRhqHriLA8CO4WOtWrYtmviyLTSuVIQQnMZ&#43;Ok9kkzbOI80CHj/ZWnzXsmJn95jAWSe2YudEdbb1hrVdF/fyX9eNn8yfPF2/ONnWjKCWTUXz/ePLw29n396dHB6M8BefB9q7hzvD3r4ywg9TfunK/EGoCt5FSKqUgGgFRcYwRD9hi5HpnnHOubfXrs&#43;Lvj07/8x&#43;vHz1bnM43Vd0orQkmUcQfP189erb80x/2//1fjx9&#43;tzfJE8a24t5apUK0gVHq9wY&#43;v2r4H0a9A8v/xxjTtEobzSgVwm8oOI7Rxe0Y/gnf7lDoHdIpfmeAkOd6vdZap2kKgSOMMGeYEIexvZoAzjmlzeli85e/vfo/f3nx5GS&#43;KUtrGuJURLya5DRbr9u6btebtmk1xvhfviejLCYEW&#43;uqqpkvC2tdniWcM9h9g/ALJqRjiIJ7GlVNO18WbSsjwdM0DlELj29CMEQRXCAVqMedmhDerdcKBGdxJMC/&#43;Hvi960Abr6qqiAcvZXSsJys1uZqAljnNqV8/GzxH//1y08/n2pVjxONkdFKO2uQRwFinEkjzxar//w7yhIxGcVxxAWnWpv5snj89KU29nBvwhiVSgd8ot7355x/MsFZniV5lpRVfTZfWes4p0VZuy1mSdAIILMMoh&#43;wkVlwcQcj34AXPs&#43;Sw73JKE8DaW4R9FE5iKb1NkGn2phrCGC0Xayqf/48f/J8WdflLLeHU0Ewk4pb6zwL44xzUdb29aI9na//8eTs&#43;wfT/VlGCW6VWq3L16dLrY2UyjnUSgm5eXEkHELO2rqVGOHxKJUhbiOVRyaE68qy3lS1tY5gbL1y7FopnXVRWODA1rQ2yv/ESqU4Y6M8rZs2jkSaRn3G2S0BSmmaphAihFD5OTNAzjrHhulswCAC/7HzZXXyy2q93mDXMK/2JJNRDoY1RB6Cw39Nsayke31Wvnhd/PDdLI5Y8EGZKBIYq6pqmtbLHxZYDaVEG6uNNcYKT8IQG8AIdNkuvooRo1Q7Y4MCFuKxRinFGE2TKKi9qKrbYlMBR&#43;KMcUaB27pORtwWAJ0e4p2X8/4A3119wLl5FoigtFlv5GJVS9kKorCjUcSODqYIoaKsGaXjUVrV7dmiwE4hh6tGnS2qsmz3pyklZDrJheBa66Ksy7JGGMeRyLMkjkXTeKUAYZREEUiILI20tm2rmlYCltHYP0DdtNY6SkjTSq3NKE8ChyHOoTiO0jgy1jJKR6N0lCecsTSJb6E2fFk16Fc8cFQmA3&#43;AFdSFGBHo/rqR2lqNiaWUj7Lk8GDql6RnJvRwf1LV7fOXZ8SvYKe1qWoJOXhJHB0dTDtW08hWhiQGRgXnhBKlNGQJeibGKCaEUeqc5ZxBlFFwRgjRXnvTDnkCwD4QggveRdP2Z&#43;NWSiBP9z3BEahPnwn0CZbnBRpXZGQ40PAQoySJo/3ZOEmig70xpTTP4jdnqyjybz78RYiAM867OHCWJnYr98NKwIFNuO4DGAkYI4cEaAjb0/xJIIkxqJ&#43;I4O43yKEsjTt1ql9ieHvCZwLBJeQ3AeuzdPpjGGFKSRyxKGKEMOsIoCKK&#43;PHhDB/NMMLamMVyYx2yFrlwfhJzIRgZsDnnEKVuJ7UBNHx3fsvOO&#43;55y9ZOALIggnp9CePeoMMYvh8aFYO3sq6jXOckuUSR3ii9zkZ0YG44NzwhSE5/EBzF28fBYIK8p1ssLCe2kyQDKOGMjnMxGycnIjZKSmWrRiplOKNxLJxzRVHXjWwarQx2iKaJ2J9leeqFpBfuxlMXdQIFFnVnRPnl7lAIGJmQ3kwY67LVwpbs1Mrwnf9SKd0G1sRZ5/AIks3/H27RG2iBqzqptQxML45EFKLeCG/3FULBQvQ8kBJ/XxxE&#43;vao27rhvQ0UtDIK7jPPmf17ecVXBvWLBOUSrBnQLuANezxS8lbDsMP11WooY2Rvmnx7b/L05eps3patWqzq&#43;XJzr245Y8ba9aZ6My/WGyk1Zzw&#43;3s/uH43yLMIEK6nLupFSeYUn5E7Bs2ptQH1y1tWtN&#43;JCvhQPVSMEhUzFqm4JwVmaBJOCU0o2Zb1YbQjBaRJRQkEDTuIIY9y0/h6BV7k&#43;iqG0rmtprc3zJEtjRigmHn2c&#43;zet6mZdVHXTwkW82KCEUsIZs85VdaO1cQ7BH6M8ZZS2UhHi&#43;aoNaYdV0yqpCcEi/DakPQu/9QlBYQWAjEyTKIlBJ76aCnjLPdlgi517bzwBJtmP3&#43;&#43;9eF1Utaxl&#43;epMPnp6lufZ0YEx2vz8/OzxyfJ07SyOj/Ynf3y4/&#43;CbcRJz5FxZN7&#43;8nq/WpdK6bRVCLokjhHFVNzioQ946r9tWKsFZJDjcFNSbqm6CoynPsjhLYs7puqhevVk458LO88s9S5PZJCeErIqyrBpnXYi9mpCvyAT368MYW1Y1eDVw2D15ljjnVkU5XxZFUSGM8tSrVV47EDxNIqn0m7Nl2ypKadO0DrnjwxmjdFWUziEhWF3Lsmq86ec6ZzAwRlhAkP4ESyGKxPHBdH82iYSXkX0AbIcGBAeP8pU7gGCcp&#43;KH72bropFSP3qGzwr9fx&#43;tavXycL/Q2j5/tXr0rKpavj9L/&#43;2PR//2x6PDvYxzCujYlPWqKENo1GuK2pi2VfNVQTCO4yhkc/pFFAfs10ExDRlDNPAHAvoopdR4o8Eb3saYpvHrHUyVkFWAi021WpfGWCCAv2Ak0iQW3mKjUqmqkfDSQnDQ8aqqaVtZB702KB0hL9FYhFFdt4vVJrhDhDY6JJYZhDDsCYTQuiiLTcXDonFe8dNBJURNK5GXdoSG7BOtTdPKNPHbq5UM3C3&#43;w0U3SVe0gq&#43;pDwjKDD2Y5f/jT99QSrJU/Px8vVibv/x9HolVMG6Nsfzbb7I//bD3v/77ve8fzLIkIphYbCPBJ&#43;Nsew8cihBoUVbG2iCrI5CE3gJIYsZo08q2VTicGdiUZ9&#43;eRSRRLHji30S0UoZ0LAdHx6MUpCLBxEsO0wfyPA2yNBaCN41spEeNtU4IlmUJCzFxQvwzWOviiGdZwimN4yiORRIJY2wrFQ82JsY4z&#43;JQU0fAI0IJSVP/aEG0IG26DC1YZ/5lgcEGZVJw/y4qvI61fkWwgbe4W&#43;VehFBcFAV8hlglVAhtE2CdVHq&#43;rJ6&#43;WPzz6eL5q43fENoSjNOEH0yT7&#43;6P//Dd7P7xZJRFIIhAggUOIzGC5/acrmrasqyDP5aDWAQx6NV8a4GBgEwLrjoSFAHPTxxySukgOC1seX&#43;RkFXvubFUoPkgh0znKSJxFPHgkoLLenOfdla0lN7cA1eTZ/1BzoYEQwpZ44B6YyxoffB4IE7bVmq/oQlkY3qSh&#43;cJ2qR/AjwI5XIeRHS4EeMsDk7HQRa7kVKCCXwTAbbOPFvVcrmuF&#43;u6rJTSNnAPOs6j6SQZ53Ek2JDFhRs4eG7QWhByRntmsk0UQF1iRiBF52S&#43;sAE71x2cunWcnO9OUEPd9rW7j0EG4kBaUMY6odblKHhseWR1hkhvcmx1IIfgIO4ji6SzSOB54Ye9LdIpdReDH&#43;eu8u6s7s9h3HCHAG9JzvUCmdMRjdNEHB2MDDiEw2qlhFDWLfD&#43;fOcfFCmDtAn8B9Qzhy3CJKgE1vMNMJoC1uBRSW9SwSOikMm9Pbp9L3jj4ClxSntce9LToKF7wYgYQ&#43;TmSkiKr6gl694UEXTdwfDTmzH1vvD27Ggc1oLHNqNDXfdyWARw10q3Kk3dWkpwFPaSNq6VYFHjsEIxZ5hSBCUwODjjIt6d3F1EuVZZHjR1pd3W8e8Cm0JlbevW84osITEnyjitXSRwnlDyAepuf1d4h/T0re13EzjrWmkXhV4WxloEvmGlXd1axnAWE1jCghPOkNIo&#43;HNwJPD&#43;hM1GOKQL&#43;81etXZdGsH87Ta1lQoYMYq4J0lZm0Z6flGURHDcSL8h9ieMM3xZ1t1y&#43;KB9CrZpeao1xUZVjQX8IoyUsrEgyBCpnTYuFsGr3FilneB4nNGIuTxGjFAU1rjSblNZiIItN6Zq/N&#43;C4zSiSYRb5aTy16kb45DfXoziUUrAULhNDum3w4cjgLVOSVTXdCPjWmatxq3XoDn3drlGTljMJebKGeuEJY2ysjDGuCSlHFG85o5HLk9wHGFEGcWMImU886EEU4q18UiPuNvmwlhmgjwwCAuURASCBZ8X9j8cAbz6Ke3pa/PiKTlbjEvJGmuMVymCfw4Z64UkweC880ja1JYVXg2Y5nSU0WzO0JvcfvMNvvctyUaeWc24thCccE1r/XYxLk/odOQtfKUhiwJDFIMznMZE8M9MAHxQArS1&#43;eWZ/o//7Z79HDUNsxfTUkC/G2hwQrpUelU6EURwQjm1e/v6v/1PkmYkyWJBBaeg3VmHtPYy2RiP6DjyW&#43;rcE7r1qQYCf7UE8BanQ0q5ukJVQdoGD1R7ELwDQ8H/lzoXBxFNNPZaqmaoipBsnDVe2wmqUX8&#43;4B0uQraa&#43;JDZfG5oP4cPRIBQx0YOjtmf/p0cHCOtdm2rq&#43;Ach2BZZCP64CHJRphQdMFt0hluF&#43;/4YR78k8OH2gEYi5ge38dpjtrGud0i919zAcwYTnOcjdAty2z4qPDBdgCiFGc5TVJ3qcXAr78I9oycIHxHgPcAzykoIhT/Gu5zzSU&#43;2MN8PnCBAH3GxCd6mK&#43;xJue8QKN31N2Vx3xU6GvNIZxwoUTpcmeTO/gY0DfU2yUAxJWGBHgbO&#43;q8yujCafi9mMnXQvRhNdgVQngoBrYxCten6p8HT7aRDMBal32FOux3&#43;Sidnh&#43;SZ9AwjnEhDrO9yDbD6oumw&#43;UFfU6AC2gI9cPa6LpplQKmRMA3CVn5EP&#43;E9EJrnZSqS14LgRodepZCfKrXbSglyKEQZEc05NtAKBUCk5SSSPjLfdkEGFaKwTdX14gBruumPZuvy6pRSuMQlQ5x1tCJidI44kkcpUksldqUDSxezlkc8VbqsmogYNtXi4eANVJaU&#43;JxHZKfTegnyzDCcSxmkxFjdNfi/eJgJ2HrphIlpXRVt0VZV5BExZkxVioNqQlS6bqRVdO2rYKEKkJInsYIoaaRZVmXdRtabNKQ4axg/0GabRyiX4wSFjIhpDKEkkuh4a8CbjLESEhc4IxFwpKQfqyNDXlkNM8ShLGUSkptrQ1JbCTkmsdRJIw2nLPIC3q/wGlIjYLkkW32LmOUQAoJwlgpnaWx&#43;NL5z5VwLQEgx2Y6yZNYWOsg8ShkhzjBWOy/tHUjrXMhbcb/6/lSLCCP05PB2q6imGB3Lv0dDmcy6kUI8J/QS47zOwLs9C2KBJ&#43;O87BsUZ8p5MVpQKqzLs&#43;0g1I6TEArgqAUFH/11xzmpnfF/EFck20NPwjwy8l7XyTs9OG4YAfs/BFSUFmfqL3V97fLlCKoS72swgvCHOudzeeVpW6bV36VsrmbJf&#43;lwk4z8QtqKMCwy8Sne84vFvo6O/h4gQXttJW8g48E0N62628OX/XLH3ob3hHg48GwbP18gMOwiPLX1PxvOfnv9dRfFlwwxPq2tpebZfcAR3vCQAUvco4E3RSuCSeCpmRDVReBhNg7Il0PUEffcSFwUu&#43;UEW9NYtXVkAqBManqJtRfuCyNoRCn98BZ51qpmkZyTtMk5kHhv4Xlu58c&#43;tV8vgOG01H6KlZoAFhVVdM00HjZOvxmvnr2/LUxdjrJ40iEZoA4FK8ZjFAZytjzNJlN8zxLp&#43;M8TaPPqIL3d4Be23TOsWHr4/6MfvhHXddKKZh8AYMnlNRFUS1XpXOuqtu6acE3VzWtlArcFa1Uozx5dRrNJvm/PHwAlQ6f7n1vKVwdD&#43;iPQfMGGDkhhMiyDJpcYwV17tRvjlBup40RjLWhgDO0hCBJLCghxaailKgQYvvMMmY/Plzrju4B2nwA90&#43;SZDQaRVGEgyd5Nh21UmmtCSHTcWaMhSo4a10c8p6hEUtR1pNRliUxvZMB18O1O4BS2o&#43;K4AHAQuac7c9GkeDGWhq6KUERFjAuKJ0NPWZU07RpEo/ylHxNiVbvCtfuAKABFKQNLQNKSKhw5n3x1CDyiLrCo6CnhhpVwjn7Srxs7wc3xQN2uhz3EJzPN5VMOa9dnTfh&#43;0CP&#43;mXCR5nk2RdB3sFb4Y47f2J4fwJ8&#43;Z773wXemQVBPbQxxgWBfJXjblCifOF3yCFHBvGGO3hnArgwLHdT1euigg4mwqunoaI8OIKAIH3d/tadhKANmguGQhKLOxr08I4ECL62s/n6&#43;S&#43;nzrksTbyNFZJ/QjKW4YwyRnUIOIjQqxF6PITWJ37THB/MOJtwfqeadvCOLMg5bUxVN5uyxgg3rVytyzChjypt2lYKwRklVd1iTNI0MtrA0dEohbaVWRrvzUb8c6vm/XjwrjIAQ6PCJI5CB0Ww0bohyNA2R2ldNzKYYHSbykgJJmGgOuPXjLz7auHdCIAxigTfn40JxtpYQvDx4Qy6sGKMlDLWuapqNlUdC5FlMbglwhCnsDconYwzdhdzHsC7EgAzRiejDHoeEdKl&#43;kCBLsx5CM3aDR&#43;0Iwu9cbu&#43;KsCIPsqrfJ7AIEL/63NyunxmmBB5oVVgp&#43;1A8zzS9TY8/13fXeVu&#43;Q&#43;BwQRLGDAGIclfoyOe4/ASksnb&#43;pd/DdlXN8MwOY6dnDwnBKdpBhHHOPR0u9PTPyr0s8I8AV7&#43;8hIhNArjdrkQI5MHt/5H6g91B2jb6H2bmhh6ZyNjbRO6lcdRNJxUfQcfA4AFQQYie/DgQU8NSiljw459aFuStGs3bdOAOp8PdHzry77IJRF8BztwnguxLgq0LVANBS3nmUJK6app60Z2&#43;ULuvBkgFHlpY/tmzhA7M9YSjJNYJHF0Cye63AboR2VAPTbL0rSfkd/PEoDemU0rzxbFcrWRSsHiDg1ISWiLTqRUrdIh38RTJoyx9zqo4Axa3NPPrX3b7wN9nR583E4WxpiEZd4fg9Eudd02rWwaCX1vsyTGlFpnldLrTd20MhSOGeh36vkao3maZGmsjbll80RuKTBg8TtD4bceHiIEiyMBTuY0iUJDcSJDK1uoW&#43;Ks4zzO&#43;m3EBUuTmHN2VU/LO7gCWFnVXZjFGGsNgR7xoYFwEkd70xH0O2dhBlIccegzb4zNs6SVCgQACcs/CADCWPD8iLtkiF8F&#43;C//75&#43;g1VCK41jkofN1GFMBktloY0PHVBJSQEnfxNd01gTq&#43;/L24wi78Qif1UyR3w12hfDLV3OomosiNptkcVet6HbcPv0AEeRQ8DVg6Pi/00usD0W&#43;k3/pq4LdIr3JeNT1q2ckiiLK6LBdzTXFdHfr&#43;v2hL04CYAf7s/C1X9eRoHEUic&#43;x/eZnBf3Uf08A0XntQ3NtRoMdxm&#43;hGfuuKu2w3OFWQV&#43;MBB6hPiDjuXs/zrefQwVt5DE&#43;74l6cdhUlwnaS9shmrYt/gcJiu5ChbAbzgDvnuLy88JgFrQt&#43;L50skM7HTChIB&#43;WGN1OzfgouHxfuKlbSg/QM7hqbKssJTiOCExn0xaZMFWFU0yJ/6i1YwwLhq1F2jhMunbF2ri6tdo4zohgXqxDL29Ku7pvY53U0HgYMTqolB8S06FW2ab1ildoKO3/HQ4ngsva0Bo5aF5OyXZdFJswmGI0Go3HYyFubyLMtQSwztXSvlmq1cZQirOYcI7DEFnXKv/CaUwigaXyREpjMhtRpV0RWp7HAgtGjHWLQtetiwTmofu2Q0hwwmjnUNLaNdKFbvZYcBxogLaqFrIWQQvvqrGbOpTUBt8SIZhiUHZxmB2Fwoh2T9o0Jomw5Xr55MnjFy9eIITu37//hz/8YX9///YSoGf35yWN26VljEdQUVll3BlGvdIJAxPSmIxSQLoZpZSEhtq/nCptXRbTPPEEKwPuTOGa1rXKRoLEAm/n4yHbpU93jaUjT7buYYxFUtkkopOc&#43;o2iPHtspA2jGzwLpdTTg3eTn8POQ3hvTPdHZrlYPn369OTkJCwXvbe3N5lM2G3NxmACOEsYA3Ih0zC0Yo0FTiLsWmeMxztyHZqc7ZDIKI4ECdM0PWcgBDvjtHHGohjjWBCl3Vq6TW208TxEm66BOnj3kshzp1raroddGMhtradlqxxj0BsEM&#43;pCEy9PMyAAY35zGAtbIQzjwGGiWygvhHF5waGroD3eJ0bz9cBiQbfy0A67j2PsET3O/NGJ2U4UDzwEOc&#43;7YYQLvLzgOE8pzNWSyjKKk5jEnMSKCI7T2I4zghxKE8IoDhly3U2SCDOGW9lNdYg41tZvL6B3LMg4I9ahpsWho4onGAxq6KehhA753bZNY5xFxIxHx8dHWiuE0PHx8Wg0us0BPgaTYWBxXQgUhIkV44zGgoRliPox4yD3ul7mBKYEeYxY57k/aCxhaD1OjcsTj0GgDaeQvXJeVMMIwgQbv3T9hgNywsnW&#43;aOcEeucCo3WYWA9jGHHfaIFRt3YhjDHh1PE6Z778cf9/X2E0N7e3mw2u83NF3DTNEPllIeRdgMVEw27JqILIwDQ&#43;ffbw5AC3RfWuP7MweTUYWuyYTdFfN4U57y54sWujHgQhkMXLtLdI8z1slYGCDP0PNyqHQCohvDX0A64AvCgJ8R5vyCHh7joEIcuDpzqrzD8OOh32V///E7DLy95Oy5Gdq5by527ilIKU8Qvd8i7hXATAawNOrh01LMCz2GMcZ2iFF6rbxfquUdQIofSzrnhxjmf/tu79YIf&#43;6O81e3Hew83GWLauNXGnK40SD9QDcNwXSQEgUEugQxeQgjuvzEWdVUC3mRzQBvIEA2TZ73JRolXohjDo5RE4hZ6PX5XuJ4AQZ9ZbszLM6W1SyJvqIZOoF4TneaUcxzaFHh9EVRMIIl1SHgdETfeXnOcYaAl6PL9dfKUfncUpsG/LZPuy4ab&#43;oZq4xUSHMycRtrAhbyCz7e9hayzMFWwDTM1McbBIEI49eqmCwYUDPwK0zEdD&#43;ax0q6svcGs7vKPbirURpgxNMkoI7gJA0jBZIWROuOMcoYZQY10LnahexAK46o8/8kSkgjSKgubQ&#43;swz4p6w4Iz3CpXJTaNvVX8mTDqjwg3VMqjiJO9CZ7mrnfAIdSp2yKo&#43;VlMlDmfZwpjwsIkWi&#43;TQ7Y65KyDPxWBoDbBf0cpTsNAqt/tVW8n7E7QuJC4S4OpybG7PHA2KDNROHRZLdwOmsI7Sjs&#43;NyMQeJ&#43;&#43;zqjlBSTvHINygcu&#43;w10kD7MSr7jDhbKBnUPn7j6Hvk7s99MbdrsmgoUW5lLf5TR8FBhOb4BV/v8DAAD//x/toGRM9QtvAAAAAElFTkSuQmCC"
     data-sizes="auto"
     width="740" alt="Personal settings are under the menu item with a label of View Profile, then there is a settings link near the top. Image is a screenshot showing this."
     loading="lazy" />



  </a></figure>
</li>
</ol>
</li>
<li><a href="https://help.salesforce.com/s/articleView?id=xcloud.identity_passkey_login_enable.htm&amp;type=5">Register your build-in authenticator</a> (or passkey through something like <a href="https://1password.com">1Password</a>).
<ol>
<li>Go to your personal settings.</li>
<li>Select Advanced User Details from Personal Information menu (on the left)</li>
<li>Click the add button the Built-In Authenticators section (most of the way down the page) 





<figure class="figure-center">
  
  <a href="/images/posts/salesforce-security-updates-2026/buildinAuthenticatorsAdd.png" target="_blank" rel="noopener noreferrer">
    
    

    











<noscript>
  <img class="rcf-image" src="/images/posts/salesforce-security-updates-2026/buildinAuthenticatorsAdd.png" alt="The Built-In Authenticators section and it&#39;s add button is way down in the page navigation, keep going. Image is a screenshot showing the button." loading="lazy" />
</noscript>

<img class="rcf-image lazyload show-if-js"
     data-srcset="/images/posts/salesforce-security-updates-2026/buildinAuthenticatorsAdd_hu_2deda40185a9d5c6.png 680w, /images/posts/salesforce-security-updates-2026/buildinAuthenticatorsAdd_hu_b58e61bdfb80f2e9.png 850w, /images/posts/salesforce-security-updates-2026/buildinAuthenticatorsAdd_hu_2340af9b7fb1bbdb.png 1020w, /images/posts/salesforce-security-updates-2026/buildinAuthenticatorsAdd_hu_88573d5f804ceb62.png 1360w, /images/posts/salesforce-security-updates-2026/buildinAuthenticatorsAdd.png 1540w"
     data-src="/images/posts/salesforce-security-updates-2026/buildinAuthenticatorsAdd.png"
     src="data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAAAhCAIAAACeHDTXAAAMXUlEQVR4nNRaaXMcN48GePUxhyVb8Zts7frL/v&#43;ftF&#43;2UnE2jqXImqvZJLEFopvTc2hsHXH8QpZL08PmgfMBQLPddlorRCRKKSXKBH8/ISIAqEyY6cJgypvr&#43;z7G&#43;H22993IIAIinzDGGEKQ4wk7pn&#43;Xj9MnL6QsdZ5Ta/3VOXGkV1n6&#43;xElSAmIQGlQ6vR7MwyjFEIQC0BElYemlKbsLvwq7BDlff7exOhSkuW&#43;UQbHkwy/AAg/nHAoUe9htYIYYDbHugWtj4YUAVAxc0Ss65qIvPcxxuIiELFISGttMn2L8j5GKQ1STynpk50d0VnWE0EUDQNQCFrx/z&#43;QkaQEmzV9/F/abdUv/wU/VbzFQz0xh&#43;NZAPzU8POu67z3xVMbY4rOaq2bprHWfpVxF2jq6J5KBBAjbD087HDd8UkrC/Oa5jU4w2L4IYgIuh3dfYb1it5cY4pA9shOzWQwCa&#43;Fy0XNiwBE2VNKYhbALIgvd0HP8&#43;wpwaqDj3f46y3erSAStA5&#43;voL/fAvvFuS0RK/9&#43;AtL5HPT6MbOjKeB4OxOKb91fn7KGw099R3GOPrKAzLjcigsrqqKiESvq6pyzh0FQNmLjCeiEMJT&#43;HZ08sGbqXPR6eKL0AX48wv&#43;zx/42y36iArhbg0PO3aRVtOijpSiYoDBq/DRxiWOGE2jHmT9G9io8vFwHJxi8n1IlIzWRmvIXzByyV8Rgwhl&#43;BSPaBLROc4PZERThPXjPs44h78JfhQBPGn&#43;ROx8Pj3Apy8YSb2dY&#43;3wyyb9tVG/3cW3M6Kw8922GJmz1lqTX0ysyOWA&#43;YgkT2GQU8whRY1eTPRs1/kQo0KltQiSBwNBYFgMlTOztmkaZ54eEQcLUGpAPhc88hEefblIxAKeMRUR9AHWHfrArv96jm2FCGq1i6sdrDZB9dvVw5e&#43;7/sQU6K6ctbq7DzTeFi2jhCj&#43;EBj2NnarMa7zrO&#43;56RIKWwb9gMhhD7EXdf7vi&#43;&#43;Ggfohc6Zt1fLG7XUzUSTslC/epZ9DPh2XrxiHvDcN7PVKpZfTOADKEU&#43;p2iKPQPzDhFiohAix6oUyWc3koi5rFVKDKl9zyIwRks2CMAaHUL0LLoYYnTWVJVjh6SVYveQvA8EvC4l9r2Wp8M&#43;xJ7h3BhNiLcA&#43;8gy0jlxmP2ZDknU80LydcFBFdX&#43;m0gh1BauWphVdLuij3fJGuh6MIrezuGq1YuqWbTa&#43;973ERGsMQTU95llxohXyalPSpS00tZqZy0ihJia2oWYYogcToxum9o50wcWTNvUb5ZDrpqdF1irJVrM2tpaIzGHkc9mxSx1bpDEIJgEIYjuAA4xyUyZRkNWQwXvCzYtEXgqksdy4&#43;KgpvS6AkCA2sH7JT1s2a2vdug9o8&#43;bBX24oXdL07gGoc4OJom7KHmlGqJrZuAIKHIoElyX9vzNpzBmwH4p0YhBxu8RJEUiAKOVNYYPGiOtvtDHX4ES3vyLGPxIuEm03cDqAYzB2YJlk2UwJmL5xRBJkmLfbUMIgkElMxAkesp6OYNEUXkowpO8wVrrnHtJsnZeAAhWw/Uc/hto0cDdmmLC1tHNEt4vYV6jUTk7sYMPEMQyTZjL9keBDph1ql3DUsfPj1xJKROUocC48/6WtmtlLVT18HLf0&#43;2fdH&#43;L7Rz&#43;4wMaC3mPJRNmj&#43;l7RlYU&#43;9Vq3XU7hg7GdF3HeH&#43;C5GCytVJLEAGwSSd2xsYY51zbtjrTK3JfSLzQDbObdj0kIqfZLCoLGqflkwmfDtKCA/adPD/&#43;6lB/LiqTQrAOjOEc&#43;P4Ol9c5U0/kO3ZNn36Hn37mjGwkw7ggUrZLzIxSBNq6KiXOBoy1qHToh8gzZB2ZtNKCsimJmo31DIbG/K42VmkDoBK9tErDUe0ETCsEp8HWMKtg1MEfIAdGBXWDizf0x0e6vwNtKAaICbYb8DvwHbJ4bBGp2ewgcmgCowVTAaGzNSrTAEcLFoqtRs8IhHujG9yfuFMEZBYxpxhHo0KWkNI&#43;qeAB8GU1ZMmXTnDdwPQXTf3ahIiuosUVNjNa3YPW4Dt2SusH6HZgLczmLIMigM8PtOpSW8O8QT3oDxJZAMuqHQcnf7KO5I2TqvVgBCImgBz82awUKZXK4OcSaQSnMnx4wSzfgxjkGFws6eoa7m/p7jOkSCHgwz2H5eUvOF8eWMBqS7dr8jFHNlMUDHOHIPW9D5x6MBAaPStrOmWgrTXDuxRjLkbyj9TqZAoRjLG2crUWhPCCQ1lFypJCmGb8RI&#43;g63&#43;WtOZI&#43;&#43;59&#43;vQ7/fl/lBlCocd2jtfvcL5AY/YCiImzyhAZSKi0n4SIgfN6vd5u1omDMKcjOLZvpB7nXJUo9d6zhhqrtY4hlK4O5Ky/rltaqho1Ptc958oBUQmnuXqR4//FIss/SQiuwqsbvPkl3f&#43;VVg8AhMbi8hrf/gR1C0rtBTCW0VEq6ZMqFXNbKQbICDQCe4WDn1EDMOVYzXmNVoqTQvHWI2aGXNQSlX1JeJzkIcM2WPB9kPzz5ez6GrJ5MrEK6ipdv4/Xt7Hroe9x9sa8/VnVCwyE5MtI01S4TNg2jOGMPtiG1bay8zCraMQxmQ04eHkWj5Y&#43;DuXiFH8cMhYqCDsjKadZ0M8/IyIZjupjHQ0hhrTZ7jabbUxJ4n9GEIeVmCFwD&#43;Aib4rwsHPG74j3xFftqFHOZutF/NeHZGroPS6vzNV7BQZ3fopqzfUc5i06i7XDI6BPpFNCSrZY&#43;ukep7wuOHX8Gc/&#43;Cskw4y0c9VQSHu&#43;7&#43;/u/Yoy5tpMQVW4QqZy4Um5b8C8QqVxwZYQckxjuWFDD7Ehd07TO2VG9Xk4kBSmPEOF9ameQomlau1jaumG3MclKzKLF7Gf433RxDsIp9b4PFBhZ7luyrEvC4Jj7gaUjIVzWWksGl50&#43;vop151CfG0oTf&#43;P97vbzJ&#43;&#43;9MSbGYIxtmtpap5QKoe&#43;8Zw/FASlJ/zRRCn1QWhltYk6FeJfGzhfz&#43;axtm0pp/Vo2QAQhBqK426neaCJtKufqqmlrfchoYzUwr/Qhq3JFL4Sw2643203MNcWsRkpgh1b803vf5/LsNDBWVbVYLKxVZpj0VQ4EMdeyigAUojWmqhyR5OE2FyaNbJJz86wPxmgAPYQiMIMB5RqOxDHpo4gpaKVfq2KSfV/udHCAZa4pnXfHidVB82O4loJHBU0cqn0kphxD8SKD8bLn1FJ&#43;GOqmuYbHY6OmkVWvx//jVrvWerlcfvjwQUpV8mQ4XK6dSbt/fyBJGw9vPUkvyFpb13XmknRrzm&#43;7FAFK/YguHBBRKyW6WNc1ALhMp60n8/gMaK2ZzWbWWimITotxsnU5ZynGlX59VVVP7TI&#43;jfIh6prNecrlo67RcXNpBHAwFndGg&#43;CjxUkFv6DtKa8F1MH4x14Ak2kPqr/ZKnmHGUKXWzxHu7okAGGlc&#43;5s2XmqUEctzNJce5VbbOcDuJQCiXzfS/V8qMKO50wSc1M62jQQDXE4m&#43;xQjh7r1ZTHF/VKMQ68HlvG8jeM9sTLaQ3j7QJrrauq3DdGaTRqRD1Z/ZQdjwpAuqnZpURR7WlfvlwTksFFtlMq1w6nffxSr54y97SBMx1QLmec7rD3fr1a5X5h4IwdwOTBiChP5N7GoNHjvDbXyfn17KZ0rpznqC6NX3TOaWN670MI8q6IKiMLlcbaMPMkZz&#43;JKIaglJrP59JYVvsy7Ffc8KMCkE7Aer3e7XYihmJfRQByUzN3LYwE4en1TWkDTJs58sR7XyQ3hu5UrlkciVBr7ZwTT3i8v/EqB8Y42DiiNobTEUEROfSpEbxRrgmM3kADcD5PMFwHIcktM08RsdgvFr80Xs4ZFsreb0j5RaWeFcEvWYDcOpH7cVOVPBVASkkY1Pe9XFSRb4thFqfEupmFdCQAaeBM76fK7RiRWYxxagTif0VVh4V4Lcol2MHmQoxSsIIxxZISVpYKq23uQfEYwULykTecxSa3nnjD&#43;9CbO8OoJJtjkD0KiZj/KcdzZ4yVSto3QpBLMcCYIQifdeVHLkiPmxamC1vFbuQSY3E4TdMc3TEtFiCvTG8IFCPAMz0RZCzs1KRFuK/KEuyRywS9lMR475Gm/bDpraF9jjkuiac9HZq22qYl&#43;m&#43;tEn5FAEopQVFfpdOrRBJFLrQzT2c4e1fuKN6MbWvq&#43;8Bp4n62I6U7bRzS4ccLb&#43;HJDKcvwiNvPY0uxYCSXj2Pnnev6&#43;ylo&#43;MonSu1D6vNerMr3uzflP4/AAD//94KY4kGQrIZAAAAAElFTkSuQmCC"
     data-sizes="auto"
     width="1540" alt="The Built-In Authenticators section and it&#39;s add button is way down in the page navigation, keep going. Image is a screenshot showing the button."
     loading="lazy" />



  </a></figure>
</li>
<li>Will likely be prompted to verify your identity through an emailed code.</li>
<li>At the prompt from Salesforce, click Register.</li>
<li>You should then be prompted by the browser, and maybe your cloud-based password manager, to complete the process. This should be your last step.</li>
</ol>
</li>
</ol>
<h3 id="how-do-i-setup-a-second-device">How do I setup a second device?</h3>
<p>This is harder to figure out than it feels like it should be.
Option one, use a cloud-based solution (like 1Password or LastPass) to managed your passkey.
Option two, use your phone to setup your PRMFA instead of the build-in for your laptop.</p>






<figure class="figure-center">
  
  <a href="/images/posts/salesforce-security-updates-2026/registerPhone.png" target="_blank" rel="noopener noreferrer">
    
    

    











<noscript>
  <img class="rcf-image" src="/images/posts/salesforce-security-updates-2026/registerPhone.png" alt="In User Detail section of Advanced User Details is a header that starts Security Key, followed by a register link. The keyboard navigation here appears to be a mess, it may take some hunting, probably just use Find." loading="lazy" />
</noscript>

<img class="rcf-image lazyload show-if-js"
     data-srcset="/images/posts/salesforce-security-updates-2026/registerPhone_hu_dff32adfc6ecd523.png 680w, /images/posts/salesforce-security-updates-2026/registerPhone_hu_fcbfc649e87c2d41.png 850w, /images/posts/salesforce-security-updates-2026/registerPhone_hu_a6bb3e96fba88b1b.png 1020w, /images/posts/salesforce-security-updates-2026/registerPhone_hu_1360b5d138b08955.png 1360w, /images/posts/salesforce-security-updates-2026/registerPhone.png 1370w"
     data-src="/images/posts/salesforce-security-updates-2026/registerPhone.png"
     src="data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAAAsCAIAAAAigicJAAAP00lEQVR4nNRbeXcct5GvAgroYzhDWpKlPG1e/Nd&#43;/&#43;&#43;TP7zel2ziDWVec/QJVB5Q3SA4lyRraDtlPpnsRk8Ddf7qGGJmz&#43;A9MwMiezf2fe&#43;911ojonOOmTESAMi/HAkAlFI8k6yR6/KvXNGRiEgpBReisGfvh2GQ7U0XAUYH91v46Rb/9gs2Hawq&#43;OF7/ss7XlWgL/byCxMxg3PcDew9a8Vj3z0&#43;3vd9X5al1noYhnEchdfygHDZe4&#43;IxhgAGMdRBAYAPkrSOQcAwndrbRXJGCPyeyVCCFxelvDDO3hzxaMDS7CquLagXvG130oUuMYwjjx6tiSaNbZt6z0XhXXODcMg7J50LXLRey/qj4iyQATgnPPei8yISCRkjJkt7HUPoxAKA28139TBIBBBY5DKa7/3W4h85IsxqD1qzRqL1fV1UZSolLUGAZ0PPAWe/gOMRuAZFRIRAHo/uancAhBQaaWUNiYYASrN4e5lNs1RafjYLYWgNERlgD8w258J142PXhVEQ4N7CWrsgEFpNSl&#43;crT5k5Mx4AtWxJVBHpOtICoMn6IUXtQBMTvgEbyD44L4jyF63B1cY82gYlADTn7n&#43;bb8ytmfLxg7GUq8k24gMk4LLyAFBNYKrAJS/xlqfoao6Q80KEIYz34chnHonfeBdeqZfwJ0EjdxFhBGa4heyM&#43;XUCxLazLWBmR1CREggNWsDetgYpBA2lE6Zr1fR3n0&#43;hIz3nvj&#43;UfoqF9m5nEcm912u9kMYx99a0BB4k04wiAUxxIlIm8Ul&#43;XGMQZhllczg1JYFNXV8rooq/DEN/sMFWIQJEygIj3DhJcH8ZGeHaznYwLj2WNKJAt&#43;NGFueUSp5xclLJ6j8/RGQekMLLqhzoIwOnVDYqn3o49Y28nJw&#43;uVHCpEWKKg9XJCYPKkKSYWErfnT4oLx2gWl/fXIgCttQDlPRmM49j3Q9O03juMOjSOI6JkMMmRBskpNaUsSqlhGJ1zwvGoi4KqlTGmritrrXyOZCGSMCXxCwzxjBzYhUah0kExT&#43;2fTmUoCBoWtbXknYc5FoSQihKxfTi4DhYQ5cHCaKV0/DMXQOARkSkKS&#43;d28nVMF3Ap&#43;pcU81ATveeu69brddt2siyhalkjDzFPWYsxRETB9Q5DRNIv5FHXIZux1jJz27bb7Tbgvfm9WX6qtSkUWUOglaK4w1O&#43;iOriOEuYiUvFXO57tKQ2WWyexJPC7nMghnR9Nt4LBWFEUvkW9o8nf2qtJBMEAOGjSAXAZ4&#43;I8wHJgfxMUc0F0c5eZQ4GzjmRa9d14zjK8eXDiaiq6uX1jdVGtiePnIoEdF0fuRoibHif8uyBEY5Fzjkav4zISQoReL5AQpcD5rI9&#43;AKXRkSLxcJa23X1er3ZbDbOBamkSsnsw4P/YfYxfnH0J3n84GjxFH2JEldMREVRSMIv70oeqSiKsrBFQRRd2nmMQKU9do/ZeT/0w9B3ycqirSY2R2jp2fnnasy8KDgqE21V/OM3sPo4CQgYJQ3ILh5dHL261bMmt22XKleYNCVSzOFdFn6nD5b/SWIvdqy1ruuaiFKtLHdBsQBT6OBwJ9d4BonR0RAdPLp3TbN9fHzq&#43;07KCRKR4v0ICbT2zvV9L5Uf8bDyuFJqsVisVivS5WW8/sH2XprWcQqxKJL8qbUuy1KqI7O6JF2asEpS&#43;azyKG5EiTcj0nLHGBON5gWmeslojFaESUJH6RwKEtOTwo7ocsJeETUEv/5sw7MSxXil8jrl70KC4rbb3W63i1sKzOq6TmCxsDUBp7lqG3RoHINW5ZwVlOVceMraEKaZAwqSQC2PJebMPgsZUCtlDCOecwMnBSBWprXu&#43;/7AAp71XUqhaa8iAEQsiuK1y597tPcu5rCZvu&#43;fnjZt21IksVdhUwTHOsNCQQbW2pAANY0wN0LcCZ4iQlVVRWGttd773W63Xq9F2/JKcEC0msiWSltrAgiSuyeD8KnDiMHKy5KOHx54T81TpTr3SPndwwcPd3bm1pcT4uRzVqtlUVgVa4uCc1K0FH8ie&#43;77IUbgIcoGk3tJ2p2FjbDDYRiapmnbNrm45Cqqul6uSJPNnvh6C8hOgim8HFLejcmvJ096mCieyiTzfk7e0tnbzFHBHyWt9WJRS1EdAGc9dRGGivtUMzf79XqzXm&#43;HYYi7UkT4cg&#43;STlJC9EVRLBYLY0xytnJkkfqiLm3Me4i0JE9fLQDvfdd1YrMi5JwXohoJe4kZCgflDDm/8ixpr3C959M4o6PQXgWvGlKhU&#43;a1JxtZT2RSxsBMWZnkuX9XlqVzvu&#43;1HCddTwmz4E45ZlDzqiKiXM/S6UIeRmbKkNXzi75CAFMtqGnW63Xf92JcOTsSv5I5a60FYLRtKzJIaxJwjuErr7F4SejleGL1UuRIvZ3D7mZVVcvlUuoBR3funJPkSEQveft8FwRlCtKfdSusNMYsl1fS5hSYl33m5NCsNUrp5KKTazpQgknDookhn&#43;3FfsYFpUpLKnfslZ&#43;EWbJGbFy4PBeOMPFRNp0&#43;VnidVEMel5hGRMlKkgHlBZ8zJCa4Xm&#43;6ro/FHA3AzsnnYKqsxVwMY6UoQIyisNFKeByduPV05HRqOVdUMpKOdNd1oi57ThVRcXBu4S2GtJm9xdcJgIjquo7dRJ/LOU8r9lxN3pVMQkppvej4nr4kByrHkJVJcinDSBeTF1JK&#43;ZeF3FRXEPCz3W6TM4krUZLSiNO8Dt45MLTvB/nYcXRiK20bHO9cXwIpPkYTCesjMGHv3Hazub&#43;/77ouWYOLJ9WayqoqyhojCtIKGX4VCoqwweZpy56iHX0q3c1/z1n82Q85XHAUffkT7U0VknC7XC6NsZFxU3l5NkEVPc/IDDGi5to9vbCqqlgxfU6V45oYTmKxDiEkR0PTtE8Pm/XaDUMwNGNZaw9orEWFtqhIzTn22TOeS8QSqM&#43;LJvndnB2YGdqhlSSLPs/xL6SjuWV6ozGk1KKuq1Q7exnVp/JtQkFzPWv6AGmqzudI3Y5YGgPW7HjXwXZjH26v7m/p6XFoG0BFq2v15j1fLXVR1otFXRVR/6da0BltOxmEnXNN08iM0N5pxTzllmRbEnWTh9njNRFJSvENPP9SmuWtAA5hUpLHc4XHOSnACRpIHk8S4MlivHPsRhwG6lvudrhdu4d79elf1cMvdruBZgfe6bfvqSzg/Xte3ihjxVY&#43;y/3PoKC&#43;73e7nQCD5MqZ2Ubqum4YBmttURRDpGQKebAVXCyVrN8gMT7ziqxwPQEV0bPdrtntQkoVPY8gd/SepQMDzEPb8uaJHj6Vm8eq2&#43;m&#43;dV3btU3b9TD0ZrfRmyfVbHFxBe8&#43;&#43;MX16JHmbt1nj3wuBkhqHssjo8ggQUbhpjQxpOogGYMIKTcFgacXHIs7Rb9CuvlAnwTqlMDH4RAfvDqz7zu&#43;u&#43;V//ITtFssSr5Zw87Yb3cN2B&#43;vHRdsW/kHvduMvt/xwzzfvlbKS632TAAQFFUWRMqYEHAXRi03IylQF2stgEzpKyPIPRfMxK2kbiO/K88Rgyszekh8aNTZWoXn7vbr&#43;Dm1hms58uvV//wnv/wVlLR1nxRFfWW2MPt8KTnQyCM855IsFZ2DJmQVfqAvfSL&#43;u&#43;KoUCtjb2/nzNA4DlwWXFr5/pzSpxRXYAhFXTWMQ3N3PWFWa3qNnrBdU17SodF0opfFYj/qQzqGgVOnccyB7CWpeb/gVLHg9yqwWAE4OKO7VElLhTHrFEO0CFldQ1qwUE8VWvQ887tux2XJV4Zu30DZgC1MUZA1qHbtVfpzmCJ/nLQ7pXBCWCJx4nZducl8vGYNE2j&#43;UDCTPapomlVJSsiblIKWmvOzl1AFKuSLFg9SlMcZUFYbo1/dw/8n/30/&#43;6R6W13Dzxt99ii6IYXQwDAwogzNKIZHSSp2SwDkYOo5j27ap55XGntNYborVi8XiMNH9I5CUJXa7XaxAPDcgU2lECtQpyKl58CnOfpPUJ4QV3vu6rhTckNF8/2n4n782//tjMwz&#43;&#43;xUWC0drHke9bfDTJyxKMlaTBUUmYCo4ExDO9QOk1pq2myoHe6mZVG9&#43;dxd0NPAQ6aKwUZO6ruu9Z0GZc3XBRM5IngwxSfbMLjuvIO&#43;QK3jnR6X8du26nf/bj&#43;2Pf91st4&#43;rNz2VwGosrpgc7Fr&#43;xz&#43;pKJar66vVDVnUmfc&#43;PpZy6jBxvKLKGzIHKSVk0x96L1z/EQgxBNjVamWtXa83j49xzmyuYSg1FSHEz0jOFb8P4aLahRjg3CgZGzqnhx77nb//eXy49bc/B6f08S/qzQda3QAZtVh59qC0F5hOVBiylkz0P2cwyEmuyeakRLxXhDhceapvczi593p0FG/EIZHgdmKndyDqZXBN&#43;jOCp1O1HIC7TkvSI5V8Nwx&#43;HLjvebtRT/dq8&#43;C7bbCExVXxw3/rDx&#43;vrq7Z2CBMQf5xGxIUi6L8ksGIkzFAKq4pCchrs3v9rFPnTxnA7xsbRJMWi5pI57XxvL6tFGqlGbiq4iimcxB&#43;Rs&#43;jaxr/eOfvbvHhjobW1DV9&#43;Kj//IP68F8h/JJJs1f74zlfhrxPCmAYhu12K5NfqcCQD0bkfZVUqEjxOfXtlstlVVW/rwC0UmVZFNZwzqZkmczAfuoVaAwhYGh5t&#43;H1U2D9/Z3fPMIwYGH0hx/oTx/1h4/q5h3WNWj69i/fnGtJJhSUmCuwISXDUoaTvkQq&#43;ucn995XVfXbzKfILN&#43;Ld6XJKvm&#43;w1ztnH4gfj3RjRCcTMddB30HXQO7La8fw892A32nAHVVqw8f1bsP6u17dfMdVgs0BvAyX306F4TLsozdoiIV9FPbVnyL4KKyLPd6XslK4iTTb5IceM/jwF0LQz&#43;nT3FAlb0MqMgPh3/FvThOrG9baBvumvB738M4hDWosCjUzdvA8fDvG1xeY1mhJlCX/NbZ52tBe64tz4HzBnoSUi6A1M681HaPUywZ&#43;8d7/8&#43;/&#43;6eHoNQyHBx9C0/cd7MAJhkERx8vTgLTGomwKPH6O1xc4dVSLZa4WGJ9hWUVv0RHz6O2l6NzKEhlg28vz3uyH7IHlnI5HX3FxVo0zvFu4z797G//H8Y&#43;G&#43;CeXjXxTimIU5WgNBobeGptYHpZBS7bIv2OtgRjglTC4lf8ouU5FHR0HiLv3O49ksSWx&#43;S8uJjbUCr2XUQGqDVcrfSf/qwWS5ZJKZFu5DhEJgZWaiVfogzrNSERkEVjgxiIZBlIHU2&#43;kvX6zvOkAOJQ35O0tuVKAm0y9JGPjOXTfRKZZY0UGhOilRAisokzayuZMf5WGYRk36jr71RZ8fTtqGetn8QgMRMza0h3w8&#43;s43huiu016KQLilWU9d3dncBQ7700XgCg66aZ9Rz8SryV0RIZDQKAuq7LshQ0NY6jGIfcur6&#43;TlH6AudADIpM5uU30F70wA4e&#43;a15fZT&#43;HQAA///Drzj6hQSGSQAAAABJRU5ErkJggg=="
     data-sizes="auto"
     width="1370" alt="In User Detail section of Advanced User Details is a header that starts Security Key, followed by a register link. The keyboard navigation here appears to be a mess, it may take some hunting, probably just use Find."
     loading="lazy" />



  </a></figure>

<ol>
<li>Back on your Advanced User Detail screen click the register link for Security Key(U2F or WebAuthn) in the user detail section.</li>
<li>Like with your build-in it should trigger an identity verification. This could be either an emailed code or your existing MFA.</li>
<li>This should give you an option to scan a QR for a security key on a phone. Follow that process.</li>
<li>Now when you login from a new device you should be able to use that phone instead of your original passkey.
<ol>
<li>After entering username and password, Click “Use a Different Verification Method” link at the bottom of the page.</li>
<li>Select the option to “Use Security Key”</li>
<li>Select Scan QR Code, and scan with the phone you used for setup.</li>
<li>Approve the request.</li>
</ol>
</li>
</ol>
<h3 id="supportmy-it-team-said-i-was-fine">Support/My IT Team said I was fine</h3>
<p>Hopefully they were right.
A lot of IT folks, and Salesforce support, have been giving out the wrong answers.
There is <em>a lot</em> of confusion out there, so please be extra cautious.</p>
<p>For step-up authentication you&rsquo;ll need an MFA registered with Salesforce – they cannot trigger an MFA from your SSO provider.
Basically, so the above steps are really close to the minimum bar you need for that too.</p>
<p>Go do it right now!</p>
<h2 id="what-else-should-i-do">What else should I do?</h2>
<ul>
<li>If you aren&rsquo;t using a cloud-based password manager (like LastPass or 1Password), I <em>strongly</em> recommend setting up more than one device.
This is particularly true for solo admins.
Otherwise you risk getting locked out if your primary device fails or is stolen.</li>
<li>Record you OrgId someplace.
If you ever do get locked out, it&rsquo;ll be easier to get a support ticket going if you have your OrgId (also for some reason Salesforce thinks we all do this, so might as well).</li>
<li>Export a report, and see how the new step-up authentication system works.
Document that, and tell your users.</li>
</ul>
<h2 id="when-will-it-hit-my-org">When will it hit my org?</h2>
<p>I don&rsquo;t know.<br>
I don&rsquo;t know how to check.<br>
Sorry.</p>
<p>There are some folks, including me, pressuring to get more details on this released to admins ASAP.</p>
<p>What I do know is that Salesforce always pushes updates in batches. They are not going to roll it every org on July 1st – very few will get it the first day.
A few orgs will go first.
Salesforce will wait a few days, then do another batch.
This is how they always release updates.
Chances are, you have a few days.</p>
<p>That said, <em>don&rsquo;t wait</em> go setup PRMFA right now!
For those who have a holiday weekend coming, don&rsquo;t spend the weekend worrying, just get this done!</p>
<h3 id="i-need-more-time">I need more time!!</h3>
<p>Ask for it.</p>
<p>Salesforce is issuing 90-day extensions to customers who ask.
If you need more time, open a support ticket and tell them.
Then contact you AE and give them the case number so they can make sure it gets done.</p>
]]></content:encoded> </item> <item>
      <title>Announcing the Accessible Community Working Group</title>
      <link>https://spinningcode.org/2026/sf-commons-accessibility/</link>
      <pubDate>
        Sun, 31 May 2026 00:00:00 +0000
      </pubDate> <guid isPermaLink="false">https://spinningcode.org/2026/sf-commons-accessibility/</guid>  <description>A new Salesforce Commons project focused web accessibility within the Salesforce nonprofit community.</description> <content:encoded><![CDATA[<p>I am excited to be co-leading a new project focused on accessibility at the Salesforce Commons Community Sprint being held this August.
Our working title is the <em>Accessible Community Working Group.</em></p>
<p>Since last fall I&rsquo;ve been working with Salesforce&rsquo;s <a href="https://www.linkedin.com/in/kelly-hamilton00/">Kelly Hamilton</a> to review <a href="https://trailhead.salesforce.com/">Trailhead</a> content for accessibility.
Our goal is to make sure the content goes beyond just passing formal <a href="https://www.w3.org/WAI/standards-guidelines/wcag/">WCAG</a> requirements.
We are trying to <a href="/2025/empathetic-accessibility">make sure it&rsquo;s <em>actually useful</em></a> to the largest audience possible.</p>
<p>To help broaden that effort, and help the Salesforce nonprofit community grow comfortable with accessibility testing, we are creating this new Salesforce Commons project for <a href="https://sfdo-community-sprints.github.io/docs/sprints/">this summer&rsquo;s virtual sprint</a>.
Hopefully it will grow into a sustainable project.</p>
<h2 id="what-does-the-project-do">What does the project do?</h2>
<p>I gave the sprint organizers this description:</p>
<blockquote>
<p>We&rsquo;re helping ensure Trailhead, and other content important to our community, is accessible to all members of our community.
Building on efforts started during the Denver Sprint in 2025 we will be helping community members understand how to test for accessibility by auditing Trailhead content (and any other content we can influence that the group thinks is important to the community).
No web accessibility experience required (but greatly appreciated).
We will provide resources ahead of time, training during the sprint, and places for you to get started giving feedback directly to the trailhead team!
We will also be looking for ideas about where the community thinks this project should go as we get established.</p>
</blockquote>
<p>We needed something to get ourselves started, but nothing is cast in stone.
Once we have a team gathered, we can discuss our future as a group.
The description, name, and goals will all be subject to future discussions.</p>
<h2 id="who-should-sign-up">Who should sign up?</h2>
<p>I hope anyone attending the sprint in August will consider joining us.
<em>No prior experience testing accessibility is required.</em>
For people new to accessibility testing we will provide materials ahead of time, and some training about what to look for and how to report it.
If you have experience testing accessibility of web sites we&rsquo;d love to have you join us.</p>
<p>If you <em>use</em> assistive technology in your daily life we are particularly interested in your engagement.
I know full well the best people to engage in these efforts are the people who are let down when they are not done right.</p>
<h2 id="what-you-can-expect">What you can expect</h2>
<p>As a participant in a new project you can expect to have a lot of influence over happens.
Kelly and I worked out a process to do reviews and submit feedback for Trailhead content that we&rsquo;ll be adapting to work for the sprint.
We also want to have some discussion about effective standards to encourage for community related content.</p>
<p>Basically, we have a starting point, but we need more engagement to make the project successful.</p>
<h3 id="what-content-will-we-review">What content will we review</h3>
<p>We will have suggestions to help people get started.
Those will include at least some trailhead modules and some community project documentation.</p>
<p>Our goal is to review any content that we can influence which is important to the Salesforce nonprofit community.
We <em>know</em> that includes trailhead.
I expect that means community documentation for other sprint projects (I&rsquo;ve put out a call to other project leaders for content they want reviewed).</p>
<p>We hope people will bring their own ideas as well.
The only boundary will be content we can actually influence so everyone&rsquo;s time is well spent.</p>
<h2 id="some-resources-to-get-you-started">Some resources to get you started</h2>
<p>We still need to get a list of resources together before the sprint, but here are some to get started with:</p>
<ul>
<li><a href="https://trailhead.salesforce.com/content/learn/modules/accessible-salesforce-customizations">Accessible Salesforce Customizations</a>: Inspired by discussion at the Denver Sprint, Kelly made this one happen in what felt like record time. There&rsquo;s <em>a lot</em> in there and well worth any administrator&rsquo;s time.</li>
<li><a href="https://trailhead.salesforce.com/content/learn/trails/get-started-with-web-accessibility">Get Started with Web Accessibility </a>: A useful trail of general accessibility guidance.</li>
<li><a href="https://sitesuserguide.stanford.edu/build/media-library/alternative-text-alt-text-images-and-other-media">Stanford&rsquo;s Guide to Alt Texts</a>: Good image alt-texts are hard for a lot of people. I&rsquo;ve had Stanford&rsquo;s guide offered to me several times when seeking advice.</li>
<li><a href="https://opensource.guide/accessibility-best-practices-for-your-project/">Accessibility Best Practices for Your Project</a>: A guide for open source projects to help ensure they are usable by as many people as possible.</li>
<li><a href="https://www.w3.org/WAI/fundamentals/">Official Fundamentals Overview</a>: This is the introduction to accessibility from the folks that maintain the standards.</li>
</ul>
<h2 id="what-happens-after-the-sprint">What happens after the sprint?</h2>
<p>That&rsquo;ll be up to the group that joins us.
We have ideas but we want to hear from the community.
So feel free to come with your suggestions about the directions we should take this effort in the future.</p>
<p>If you have questions and want to reach me ahead of time contact me through <a href="https://www.linkedin.com/in/aaron-crosman/">LinkedIn</a> or the <a href="https://www.salesforce.com/trailblazer/acrosman">Trailblazer Community</a>.</p>
]]></content:encoded> </item> <item>
      <title>Consultant vs Client</title>
      <link>https://spinningcode.org/2026/life-on-both-sides/</link>
      <pubDate>
        Sat, 31 Jan 2026 17:36:37 +0000
      </pubDate> <guid isPermaLink="false">https://spinningcode.org/2026/life-on-both-sides/</guid>  <description>Life on both sides: Some things I&amp;#39;ve learned as a client and as a consultant.</description> <content:encoded><![CDATA[<p>Lots of people working in technology have a choice between working for clients or working for consultants. We work on one side of the relationship thinking how nice it would be to have the advantages of being on the other; the preverbal grass is always seems greener.</p>
<p>I spent a little more than ten years as a client before I became a consultant. I spent just a bit longer as a consultant before becoming a client again. There are things I&rsquo;ve learned in each role that help me do the other better. To ensure a mutually beneficial engagement it is helpful to understand the perspective of the other team.</p>
<h2 id="why-is-understand-both-sides-useful">Why is understand both sides useful?</h2>
<p>The goals of a consultant and a client organization are misaligned. That doesn&rsquo;t mean you can&rsquo;t do great things together, but if you don&rsquo;t understand the goals of your partner you are likely to step on each other&rsquo;s toes.</p>
<h3 id="the-goals-of-clients">The goals of clients</h3>
<p>Client looks to consultants for one of two primary reasons:</p>
<ul>
<li>Add to team capacity</li>
<li>Solve a problem</li>
</ul>
<p>We either need to complete a project that our team does not have the time to tackle, or we need expertise it does not make sense for us to keep on staff. Sometimes we&rsquo;re looking to reduce costs by having a group of part time people fill the roles of a smaller number of fulltime team members.</p>
<p>I like to have my team&rsquo;s staffing level sufficient to complete all day-to-day tasks and to bring in outside help to take on special projects. Other people like to have consultants around consistently to provide the outside perspective and the diverse expertise that consultants bring. Both of those strategies are foundationally aimed at those two needs.</p>
<p>A smart client wants to spend the money needed to be successful, but not more. We want the most value for our money we can possibly get.</p>
<h3 id="the-goals-of-consultants">The goals of consultants</h3>
<p>Consultants have a different pair of primary goals:</p>
<ul>
<li>Record billable hours and/or ensure profit margin</li>
<li>Have a happy client who refers more business</li>
</ul>
<p>Some consultants will protest that they have the goal to solves client&rsquo;s problems through good work. My perspective is those are way to achieve those two goals. Some clients are happy when you do good work (but not all). All clients are paying to have a problem solved (see above).</p>
<p>Profit motive isn&rsquo;t evil or wrong – even when supporting nonprofits and other socially beneficial institutions (having spent much of my career in nonprofits, we think about this <em>a lot</em>). Consultants need to make money to stay afloat. A consulting firm has people to pay, overhead to manage, and founders/investors to reward. Independent consultants need to eat, pay their mortgage, and so on. The larger the firm, the more pressure there is for larger profit margins.</p>
<p>To get new clients, consultants need &ldquo;referable&rdquo; clients. That means having clients who are so happy with the work done they will serve as a reference. I wish that always meant creating the best solution possible. What it means in practice is building the solution that makes the client happy. As a consultant I gave clients my best advice, and when they disagreed and insisted on a different solution, we build that instead. If they ran out of money along the way we still tried to keep them happy, even if we had the duct tape the last bits.</p>
<p>In the end, consultants build what clients pay for, and that&rsquo;s not always the best solution.</p>
<h2 id="finding-the-balance">Finding the balance</h2>
<p>With consultants trying to make the most money they can, and clients trying to get a successful solution for the least money, there is an inherent tension in the system. Still, there is a balance to be had, where everyone wins, and great things happen. The trick is to make it a healthy tension that forces everyone to be better. Finding that balance doesn&rsquo;t <em>require</em> that everyone involved has spent time on the other side the relationships, but it certainly helps.</p>
<p>When you understand the needs and goals of the other side of the relationship you can adjust your approach to make sure everyone is aligned to win.</p>
<h3 id="lessons-to-take-from-being-a-client">Lessons to take from being a client</h3>
<p>One of the things I learned along the way was that a lot of the advice given to new consultants contradicts what I knew from being a client. Spending time as a client gives you insights into how to best serve customers that many pure consultants don&rsquo;t understand.</p>
<h4 id="be-the-consultant-you-want-to-hire">Be the consultant you want to hire</h4>
<p>When you work at an organization that hires consultants you see different approaches taken by different firms. You learn your preferences about what you like and don&rsquo;t like in a consulting partner. While no one style is the best fit for everyone it&rsquo;s unlikely that you are so unique that there aren&rsquo;t lots of other people who like that same style.</p>
<p>Default to the Golden Rule: treat clients the way you wanted to be treated by consultants.</p>
<p>You can&rsquo;t always do that 100% of the way – sure as a client I want everything free, but that&rsquo;s not reasonable. But by approaching the client the way I would have wanted to be treated consistently went a long way to helping smooth over challenges.</p>
<p>Start there, and over time you&rsquo;ll learn to adapt your approach when specific clients prefer a different style.</p>
<h4 id="be-honest-about-your-limitations">Be honest about your limitations</h4>
<p>Do. Not. Lie. To. Me.</p>
<p>Do not guess without admitting it. If I wanted made up answers, I&rsquo;d ask an AI.</p>
<p>Consultants always want to appear to be the expert in the room, and so they feel they have to answer every question. Too often that leads to consultants making up answers to show how smart they are; clients will catch you eventually.</p>
<p>One of the best ways to build trust with a client is admit when you don&rsquo;t know the answer to a question, and then come back later with the answer. Do not say &ldquo;I don&rsquo;t know&rdquo; and leave it there, go for some form of &ldquo;I will need to go look that up/ask around/figure that out.&rdquo;</p>
<p>Great consultants find solutions, they don&rsquo;t always have the answer right away. We can wait for you to do some research when we stump you. That is a lot easier to explain than when you have to walk back having given us the wrong answer.</p>
<h4 id="focus-on-what-the-client-needs-to-succeed">Focus on what the client needs to succeed</h4>
<p>Clients should always have an outcome in mind that supports their work. Consultants are focused on the solution they are building. When everything is going well, that solution <em>is</em> what the client needs to support their work. If those stop being the same thing you have a very big problem.</p>
<p>Both clients and consultants can easily forget to consistently re-check that alignment. As a client and as a consultant I&rsquo;ve been part of projects where the delivered solution didn&rsquo;t solve the actual problem – even when it fulfilled the spec and SOW. These moments frequently lead to energetic discussions that often become loud. No one wins when that happens.</p>
<p>Regularly check with the client, and with yourself, to see if the solution will solve the client&rsquo;s problem. When you see misalignment raise your hand early and often.</p>
<h3 id="lessons-to-take-from-being-a-consultant">Lessons to take from being a consultant</h3>
<p>Of course consultants know and learn stuff that isn&rsquo;t obvious to any given client. Consultants bring wider experiences, different perspectives, and a different energy to a project. That is part of what makes them valuable. Clients should hire a consultant they trust, and listen to their consultant. Think hard before deciding you know better.</p>
<h4 id="always-learn-new-things-even-if-they-arent-important-today">Always learn new things, even if they aren&rsquo;t important today</h4>
<p>As a client we tend to learn deeply about the tools we use and our work. Consultants work on a lot of projects with a lot of clients. Along the way they use a lot tools, and see at lot of ideas. That creates a culture and need for constantly learning. Often they are learning about things that don&rsquo;t seem useful right away.</p>
<p>The higher the role you have as a consultant, the more you are expected to be at least conversant about technology you haven&rsquo;t used yet. You also need to be conversant about the work of your clients. That&rsquo;s a <a href="/2025/thoughts-on-professional-development/">lot of learning</a>.</p>
<p>I had good learning habits going in to being a consultant. They served me extremely well as a consultant, and are serving me well again as a client.</p>
<p>The broad knowledge of a consultant is extremely useful and everyone benefits from more people knowing more stuff. Having that breadth of knowledge also helps when you do run into the places where you don&rsquo;t know something. It gives you the confidence that you <em>can</em> go learn the next thing you need to know quickly (see <a href="#be-honest-about-your-limitations">Be honest about your limitations</a> above).</p>
<h4 id="know-how-to-work-to-a-deadline">Know how to work to a deadline</h4>
<p>Consultants are always working within time and budget constraints – usually tight ones. That forces them to learn to be efficient. Sometimes that means they cut corners (see next section) usually that just means they move fast. Good consultants have a high degree of dexterity with their tools, they learn to line up their work to knock out tasks, and they learn what&rsquo;s needed and what&rsquo;s just nice to have.</p>
<p>New consultants often feel like they are sprinting all the time, but experienced consultants learn to balance the sprints with jogging. The pace is nearly always high (at least if sales are going well), but it still ebbs and flows. Consultants learn to hit their deadlines, but rarely are ready to deliver early.</p>
<p>As a consultant if a deadline was far in the future it gave me time to do careful work, balance other clients, do research, or just time off. Far off deadlines gave me time to recover from sprints and make sure I had the energy for high intensity moment. That intensity is important to driving client success – but hitting the deadline is more important.</p>
<p>Hitting deadlines is also important for a client to do. Consultants need you to hit your deadlines so they can balance their workload to hit their deadlines. They may also have penalties embedded in the contract (see <a href="#read-and-understand-the-contract">Read the Contract</a> below) that could cost you time or money over the course of your project.</p>
<h4 id="perfect-is-the-enemy-of-the-good">Perfect is the enemy of the good</h4>
<p>Okay, <a href="https://en.wikipedia.org/wiki/Perfect_is_the_enemy_of_good">this isn&rsquo;t something just consultants know</a>, but it is something consultants often learn to deal with the hard way.</p>
<p>Consultants need a solution that meets the requirements, fits in the budget, and pleases the client. They are not there to create a solution that is perfect, or even elegant. In any project there is a balance to be had between carefully polished, and just barely good enough to be successful. Consultants learn to thread that needle. As long as the project is successful that&rsquo;s a good thing.</p>
<p>I have seen developers spend hours, days, even months, trying to build to the perfect level of abstraction, with the perfect naming conventions, and drive for the perfect code, only to have the project fail because it&rsquo;s overdue, over budget, and was outmoded by someone who worked twice as fast.</p>
<p>Yes, we all want good solutions to our technical problems. But no solution is going to be perfect. You should aim for perfection and know you are going to miss. When you learn to accept that, it&rsquo;ll be easier to move forward and be successful.</p>
<h2 id="things-everyone-should-know-regardless-of-role">Things everyone should know regardless of role</h2>
<p>For all there are things that each side brings something to the table, there are habits that everyone should have as part of their role. There are lessons I learned, or was taught, in both roles that are super important.</p>
<h3 id="read-and-understand-the-contract">Read, and understand, the contract</h3>
<p>Everyone on a project benefits from having working knowledge of the contract. In the end, when push comes to shove, all that matters is the words on the paper. You can usually avoid the pushing and shoving by understanding what everyone agreed to up front.</p>
<p>The biggest issues I&rsquo;ve seen on consulting projects was when one side, or the other, didn&rsquo;t pay attention to the agreement.</p>
<p>Sometimes this happens because everyone is working in good faith, and no one remembers to amend the agreement when needs changed. In those cases you can often recover by continuing to work with each other in good faith.</p>
<p>Sometimes this happens when someone signed a contract they didn&rsquo;t read and understand. I once had a client yell at me because I added a paragraph to the contract outlining the resources they were responsible for providing and he didn&rsquo;t read it before we asked him for those resources (these clauses are really standard, and the one I wrote was extremely simple).</p>
<p>If everyone on the team takes the time to read and understand the contract it greatly reduces friction. Clients who understand the bounds and assumptions in a contract are able to get the most from their vendor without creating tension. Consultants who track the required deliverables of the contract don&rsquo;t frustrate clients by skipping required elements. It doesn&rsquo;t take long. The more you read them the faster we&rsquo;ll be at reading the next one.</p>
<p>Once you have read a bunch of contracts you&rsquo;ll know what&rsquo;s normal and what&rsquo;s not. At this point, if I don&rsquo;t understand the contract language I see that as a red flag even before I send it out for legal review.</p>
<h3 id="discuss-problems-and-be-solution-oriented">Discuss problems and be solution oriented</h3>
<p>Projects go best when everyone is open about what problems exist and then pivots to solving them.</p>
<p>Technology should be deployed to solve problems. That means starting by talking about problems. Being problem focused at the start makes it easy to be hung up talking only about those problems, or about new problems that come up while solving the first problem.</p>
<p>Having a good problem statement is critical to creating good solutions. But once you have the problem outlined you need to focus on solving it. Yes, raise problems, concerns, challenges, threats, weaknesses, etc. Talk openly about all those things. Then make the pivot into problem <em>solving</em> mode once the issue is well understood.</p>
<p>The best projects come together when when everyone collaborates on finding the best solutions to the problems at hand.</p>
<h3 id="quality-matters">Quality matters</h3>
<p>Everyone needs to focus on the quality of the outcome. Consultants, for all their fast moving creation of imperfect solutions, must still do good work. Clients should hold their vendors, and themselves, to high standards.</p>
<p>Every message that goes back and forth is a chance for misunderstand that gets in the way. Every input into discovery and every deliverable is a chance for gaps to form. If anyone takes their eye off the ball mistakes can happen and the solution no longer threads the quality needle correctly.</p>
<p>Mistakes will happen, and everyone will have to help course correct. But the higher the quality of the work done before the mistake, the faster it will be to recover and better and overall solution the client will get.</p>
<h2 id="the-grass-is-greener">The Grass <em>is</em> Greener</h2>
<p>One final note on the way out. If you are trying to decide between being a consultant or being a client, I recommend the switch – whichever you are today try being the other if you haven&rsquo;t yet. Not everyone loves both roles, and different roles have been right for me at different times.</p>
<p>As a client I loved what I did. We were helping make the world better. I was pushing things forward and helping the organizations succeed. But eventually the things they needed me to learn, and the pace I wanted to grow, weren&rsquo;t aligned to the organization&rsquo;s needs.</p>
<p>I&rsquo;d been there a decade, I left on great terms, but it was time to go.</p>
<p>When I first became a consultant it was exciting. I got to work on a variety of projects, with more  technologies than any one organization generally needs. The pace was higher and I was frequently pushing myself in new directions. Consulting gave me insights into how different organizations worked (for both better and worse). And I made more money.</p>
<p>Interesting work, exciting environment, more money, great!</p>
<p>As a consultant I spent less time in positions, the billable grind was exhausting, I missed being focused. When I returned to the client side, I got to focus again. I have one org to worry about, one set of organizational politics to understand, and so on. I get to learn the work of the organization deeply again  and really understand the market we serve. In my case I, again, got more money – but that was at least partially luck as much as anything; consultants are often paid better than in-house team members.</p>
<p>Focused work, no billable hours target, calmer work environment, great!</p>
<p>Each really does have it&rsquo;s advantages. But so does understanding what it&rsquo;s like to be the person on the other side the relationship. Try them both, learn from both, decide what&rsquo;s the best fit for you.</p>
]]></content:encoded> </item> <item>
      <title>Empathetic Accessibility</title>
      <link>https://spinningcode.org/2025/empathetic-accessibility/</link>
      <pubDate>
        Sun, 28 Dec 2025 18:17:49 +0000
      </pubDate> <guid isPermaLink="false">https://spinningcode.org/2025/empathetic-accessibility/</guid>  <description>Accessibility beyond the scanner means leveraging your empathy for other people and thinking about how they experience your content.</description> <content:encoded><![CDATA[<p>Accessible design is about people. Real, honest to God, human beings. They are our friends, neighbors, and even ourselves.</p>
<p>This is not a technical article, about how to implement accessible designs. It&rsquo;s about the thought process content creators should go through to make their content useful to as many people as possible. For those of us who don&rsquo;t use assistive technologies on a regular basis, creating accessible content can be an abstract concept. We get tempted to focus on passing the scan, avoiding law suits, and generally treating accessibility as a checkbox. It is easy to forget that making your content accessible is about making it useful to people.</p>
<p>I&rsquo;m not the greatest expert in the world on accessibility. But when I recently noticed a gap in Salesforce&rsquo;s training materials it drew me back into thinking about the details. Instead of focusing on the strict rules of compliance I have been focused on making sure the content is <em>actually useful</em>.</p>
<h2 id="accessibility-gaps">Accessibility Gaps</h2>
<p>During a discussion at the <a href="https://sfdo-community-sprints.github.io/">Open Source Commons Sprint in Denver</a> this past November several of us got into a discussion about why so few people in the room knew how to make sure their screen flows and other custom elements were built to be accessible. The room was full of people who know the platform extremely well; if they don&rsquo;t know how to do this then most Salesforce administrators don&rsquo;t know.</p>
<p>When I am in a room of experts who have a common point of confusion, I assume there is a problem to solve. The exact problem isn&rsquo;t always immediately clear, but the existence of one is. If people with 20 years of experience on the platform, which is rightly known for taking accessibility seriously, don&rsquo;t know how to keep it accessible: there is a problem.</p>
<p><a href="https://trailhead.salesforce.com/">Salesforce&rsquo;s Trailhead</a> site provides good <a href="https://trailhead.salesforce.com/content/learn/trails/get-started-with-web-accessibility">content about accessibility</a>. So how then does the community not know how to put this information to work?</p>
<h2 id="closing-the-gap">Closing the Gap</h2>
<p>Fortunately, one of the people in the room not only creates that material, but is responsible for helping ensure the content is high quality. <a href="https://www.linkedin.com/in/kelly-hamilton00/">Kelly Hamilton</a> immediately took it upon herself to review the Trailhead content on accessibility. Kelly found, and started to address, some specific weaknesses in the existing material but the immediately obvious gaps were small. They didn&rsquo;t explain the lack of understanding in a room of experts.</p>
<p>I wasn&rsquo;t convinced the gap was in the material directly about accessibility. I offered Kelly the next challenge: does content that is <em>not</em> focused on accessibility encourage accessible practices? She held my feet to the fire and challenged me to help check. That answer turns out to be far more mixed.</p>
<p>The core issue I am finding is not specific to Salesforce. Trailhead&rsquo;s content, like many other similar resources, is divided into content about accessibility and everything else. That means articles about how to create custom elements do not consistently encourage practices that ensure the platform stays accessible. Writers take unneeded short cuts to keep the content simple. The writers also often assume the people completing tasks are not using assistive technologies themselves. The description of how to complete the task technically meets accessibility standards, but there are details missing to complete the task for many people who need those standards to exist.</p>
<p>Instead of having best practices for creating accessible content be a background hum, the content builds in assumptions that unintentionally send a message that accessibility is an after thought.</p>
<h3 id="a-quick-example">A Quick Example</h3>
<p>One of the articles I recently reviewed covers <a href="https://trailhead.salesforce.com/content/learn/projects/quick-start-lightning-web-components/set-up-visual-studio-code">setting up VSCode for Salesforce</a>. At the moment there is a screenshot at the top of the page that shows what VSCode looks like when providing code auto complete (I reported a number of issues with this unit so it may change in the future). The alt text reads &ldquo;Visual Studio Code&rsquo;s code completion in action.&rdquo; While technically accurate, and it certainly meets formal standards, it is completely useless to a blind user wanting to learn to use VSCode with Salesforce.</p>






<figure >
  
  <a href="/images/posts/empathetic-Accessibility/ExampleWeakAltText.png" target="_blank" rel="noopener noreferrer">
    
    

    











<noscript>
  <img class="rcf-image" src="/images/posts/empathetic-Accessibility/ExampleWeakAltText.png" alt="A browser screenshot showing bad alt text for a screenshot of VSCode&#39;s auto complete." loading="lazy" />
</noscript>

<img class="rcf-image lazyload show-if-js"
     data-srcset="/images/posts/empathetic-Accessibility/ExampleWeakAltText_hu_ef3f96469babdf1c.png 680w, /images/posts/empathetic-Accessibility/ExampleWeakAltText_hu_feda4f3a5745389.png 850w, /images/posts/empathetic-Accessibility/ExampleWeakAltText_hu_5139a461d6081e31.png 1020w, /images/posts/empathetic-Accessibility/ExampleWeakAltText_hu_3ca6621b3e2852ec.png 1360w, /images/posts/empathetic-Accessibility/ExampleWeakAltText.png 1880w"
     data-src="/images/posts/empathetic-Accessibility/ExampleWeakAltText.png"
     src="data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAABMCAIAAAAqdihTAAAZVElEQVR4nOxdS48kuXFmkMys6sc&#43;ZnTRwQI0Bx0EAf7zPvjkH&#43;CDdNFlBRiQ4MdBtrS7453uqswkIwzGR0axsqpnu3pqdmYBE1JPdxaLScb7yY0i4v5/fLoRL5otOj7aZhwR4ae0cboBm/bMBTE&#43;wmavMy5AADOnlHLOBgj7xXuPQ7KO1ZkBSjzx3vdfxENbLYQQYwQCUkr7/Z6Zvfc2gZltkVP0rBYPIQw6bHuf4XguAkQk5zzpMIASETOLSNDBzMuyAGQhBAO9QcrrMALHQ/yJX8ZxBOCYeb/fv337dlkWIADL4nUr7No6IQSslnMmou12e3t7S0TDMHy2CPAXze4BamRuv9inqye9HOh/Yp2n5EwvQOyNBmJjhVN02jrUjQ8G1McaFyhh7/1mswkh9DAFIAxMOWcwRC/HTTL0M88KdHASJmy3W&#43;89aNn4zKRcD30TerY4nsQYh2EwXvwI0LvCoGVZnjn1KYI9nbMSERcRYD8ZgD5dZ0Xv73npz4ADno8AjPcg4KwcWI1T4bMSGs&#43;n1qcsopUEW6129lufEEOxp7LnDNhCECy9nOlhah/1ihoPeynRPz/VJaemFAQLdDL&#43;xNchtUw6rZ73CsM51wtJfAUzezn5U454EfIB/cfHx3mee7kP2zTGiDlYEw&#43;hM3DOGCPADRgBFgZ007G9iMd78a5lWUQEdqrZxLBcIehzzkBAztl7P44jEeWcl2WBiUVEoB6bn3OOMd7e3t7c3BhWfspxmSOGTe92u4eHB6hHHC&#43;lNI4jzjDPc0rJGAVAx2QAC9akMUduw&#43;xFIKAHB9TpNE0ppRACrFX8ubKOgDZDlXMu6YBVOo6jMV9qY7PZOOfGcQRWfuJxMQLMuogxgmwBzXEcN5uNOU1GZUbUgM4wDKBuEHLOGdDBNMNWL7LMpTJYg5bNYzApimmGWkMJHsLEWrEmPv0ktI9xGQIAo9vbW4ADAsekKh5ut9te760MFcNQb1P2joVhq/cYIKbxLsPWzc2NiTLz8iDK8Xyl6s09tJfaTmDy/qg6fMpGOD3v2Y/6T03AXswBfUQIq&#43;DMABwGdFqvn42cLVaB0&#43;IjUwxGznbUHiimXSDc&#43;gnYhqncPlKyMl5XAhDbw4JAcG8O9IEp4M9UV/9Rv21TUVgEamm1oMVIiky&#43;FPo55/1&#43;//j4eBo1g/Rg5nEcY4yQ7Dhk1AEdCJUIDPWKAR&#43;ZAjAlaQIaqhUIMPzZfKhiEbFwSE8rJrtijPM841umrjebDZ5P02SScBXvAtQs3NKTCB5CDMBXTSkBYVCQKwQMw3B3dwfKe4kOwDeNfHrLsrdq&#43;siEkSfwBDoCrzwli05jGMZqK&#43;nUk/lp&#43;KEPBRrPraIpppONfY2YjI2M9nuaMwa1bRh8eqHX87Sdru7h4eHh&#43;dAHfe12u/1&#43;b9wKujOqxOuNNHAkMz9MdIC6QVY9a5&#43;6Tgb6XqyfFfHrs3VCz7ZqppFJklN/BQxnlq45Cr370sdgekPDANKLO9gsvbtjB784H2Db6jkLCICGhPABPgALCAfwqdECKA5bNKBg0/iubRTfNZyd&#43;oArwjc9bLAwYQhp472fpmm/32NvgGlPrZCiIjLP87IsMcZxHO3gpi2wPl4HA8Rc1F5CDMOw3W5ho/fcgN1ebAUZi62Ww1aAAAAFW4QVDxD3loYRjlFlv5oZNkAe6MsOZkaRyWIbfcTb3tgzFr5rgAO5mKthRp3NNAVjPk1vaNiR&#43;/C7sYut3E9egfRiHQDD2VY3/jUg4t3GhiYKDTe9GOlpFsue2jZ9msWg&#43;ZRFuApgAO6W5OmtVWy4d9SNXcyYMV/B5FvvrK3ifSt27NNH70kKXSyC5nn&#43;/vvvv/vuO2RmngpJ9ls5G2k5G8I0tr009fiCcRZ5T0XroBi&#43;/PLLV69ebbfb3gp4zliRRT8uQ0DO&#43;e3bt998882f/vSn77//jll6fj/N6Dbjb4ghUkfLp2e2mIRlPT/2WFHPKen0k0MIX3/99W9&#43;85vf/va3MDSvRRyXIYCZ371795e//PkPf/j9f//1r0S02W5DiE1ieKDAOYGOxdY3m3EzboZxGIaxl5W9xJ91IOVpUvjzGTHGX/7yl8Mw/OpXv3r9&#43;vUVWfMCBJjT&#43;O7dw7ff/v1vf/ufEMLt3f0QBwGXee/EAbTLUqApMDzGcbvdlLG9sXCb4cCVycteB4yTzxABcFzfvXsHqro0y/Se8bJQBKeU52WReclFChV&#43;9GHwIYgj5WEpXu08cc6eqPy6zMNuH4cH5QCIrKpxlQOKhWojc3Y/Bn8o6xce&#43;sIBoVo21hz7Ky5&#43;sRnayQ1ONVRAxYCIo4&#43;DIw/4CudizafsnXDK8zQ7ctUQKP9vp1Ay0vm9lyMs&#43;KBOKBPF2R&#43;NzX46LrGoztVXfkkoQgFBhc5ZsmNyjsuvjorAiVQYwjtSlJD3LETgCy4SKYiHWy/CUBeFoxS6noIXL47FE3sRMsfDiVD5mzA/c858iBNcHSinR&#43;4F5nXHS0QQRgu4kCMhJ&#43;SYJFebU8VRQVHOBZDV0Mjecxwkesec5zkn/cgQ4IPHr8yeOSjAi3VO3pelmL04T8TCidOSFl37&#43;iR5dpgzdf2VL/1Cb6c7iJNCuxSDephD&#43;Z&#43;PofjY4iRU4lW5Eb0XKiqAA6UYs6scXWaorV0menFCpF8kaHbodice61DhvODIxRjOUuVFmf3nDARzPlLe5mJHbHU2SGnv3EA0Dn4YnY9MPimdOrXpGXSqvOI8uRhkjD7GECV7lw/BN4FQB1fhQcGYkMo054SL3EkpecflnUVcuV4TrEKh2CyUyocoDE&#43;&#43;L3G8Lg5eIoKOdtBO5yMPlEfvvGNhSplkkXlKy5Ka7lKBVfAU493Nzc0wkIuOvXNqWKhSEVUvJL6IJhLy2Q&#43;ZBvLRuZq/bIE26TW0bUxjDKEFnFXZIILmXg41ZULv6aMUmL7QCurYnFRwy7IkIk6p&#43;GJF&#43;Iubk5uWnLKCt9k8vsgUBTRR9DQW&#43;pKsoJcCWQE/FXHkin4vsymQD2q3uhDcMJT3zUt5reux0HZWdLkPTkUYhGX5LrvGV66zv&#43;o4XuXoeRWz8rG0/QtTkke/iyTnOOWJ2av8wIzMhbTZoA8hA9pUVvDkAlFooYyiaBUOQfHknM/eO19EUPnJTi0uDVDH6BOwdjRIxUWIcYgD8MwiIUZWMajqpNGNOLAjJFU9UcMP1VNUhVc4&#43;uTg1xov1AErVSwwZA70aM/PrlC/J/VB0cGKpfJvIVuYuQ4gOdj&#43;yCgUQbSkJ&#43;xC8j6MQ3G8Q4jF7C3Cp9jILBx8UEPL6V8CZiFH&#43;LO&#43;TU9wiG5yMXmTvvQDZNj7xkusIHfSH9A&#43;c8/TdWeOonBfC4kKftUxBOWMRC4/7RPpdwgSm/VLIgXyLqgMV2bLTd83cBepd3ANm6Oo/0ce5uoOsI0PEkEv3hPVn9RovIHDVq6&#43;L1irkDIEPFLz9B5qFBMdbD&#43;RUiRCTjGntKSUi5IoiKpIqt61OLCFsfiSFpQBfkYI&#43;NDRzsbqD6uEqEouQxwVv67QPDvOxFnY&#43;aJKUkrzMlvy&#43;exgYbWVFpGAmFUTVgXcwpy0lG&#43;eJ65I7eIh&#43;g9cfROVrCEIyy9dfXwKBBCZG42oBSSChuWqr&#43;ARhvDEXqVHGIQFJR6oCjkLDoX48rjjaZ7UrUOGp9hUXdWQBgpb/cuPjvaiwSJxnzIYd5UhvZ6oln/9wFEvmah5DkUqSNAoBzyyJzRNsXWWtN/vVkKyyxS10N&#43;FMt30/9X54BMgoBmqKnUQXGkQlarHWyi0iKNmbnUurmrsc4CQIoCeD6lnQhPv/VyioVcZTQZ5/aUI/OIP149UJDWDqLNJKnqUHZ6M9sBuubrRcmp5X2t8GgQY/OEXw8P3rv1WDU4Pglc1XYskxs3og1crJmmU52hVFuf5&#43;gGDjxrx/qkRoKakRzPBbRxu/Bwcc7VCKSsX1HCFI/ZjoptZBnaotdKUQua5WIbqHB2HBXNOux1BV//E53rx&#43;AQc4H3YbDZ3d7f3I2/9Elx1QzWjU5VlDcb5Mfnb5EbYoSlljYdyStucEstRMAJZQyLa7/fzPH9uWeWnxicSQaZCD54XMmxkBpFYrKIGCaTYoPOcOVtx1/GKYtbsz2h8EAJe5h9qx/38&#43;PDoprTQHIr7VTSwCOUajkbQjCSMi1&#43;WIoICM2tR8ITwzpmhRW27/e7q8ue0j/OK44Xl6VYz&#43;5KvE&#43;WU99Ps0pJp9pLVE9b4MxcC9hqEdkTsOZGbeGahnHm/n5ZFS/ufQDoXRyw9VYv3IaMvJ73uuAABVgN6f3//i1/8Aj1yl3IAusnu7&#43;5ub4bRp5GSr0pYKy3qb4jHkFBMNA7sUy7yJ8aY0/YJD8BVTzhr8JKv6THh1K9fv76/v7cetGstfhkHeO/v7&#43;9//etf7/f7N2/evKyGsDboRB&#43;IA3F/FK4Cv2Yli1VJITNxES/MnGVtea7GR3ECsOdXr169efPm/v7&#43;unxAP/zwwzOnwg3Z7Xbffvvt27dvp2nqc43PX&#43;TIo61p/eM&#43;t0MOoBUEufWc5pedefUp9Gvm7gnJdXoDwumRwLhff/3169ev0dF3LSagf/&#43;P/zrzuGyFD7&#43;2p1ChmuZFn4JYYO3oq&#43;4QVrQvWu6pC0&#43;uO&#43;v6ZhirxjngC1VddNS8R2dStWcR4Kx7st9w&#43;50sdm1PqKZOnesK/1tq3tuOenusw&#43;VTAF9/EP/1939c7bXAlRfHiyZSZZWA1DJm071irUE1d9LqD9rW63HMcCxr154ha40vLjAkDBE6CTQ5XBGTvQ&#43;oWIETp6Wl6Gdi5/DEnwW4JbYatkSj0wwIwuMOIfZdNJo8oG5BlH971SyHfmbMsUpkwwEenprIR&#43;A9/jT&#43;0z//S8vEaSAlc1qmPO&#43;8m2MgKV7P0ofOLUOkfQcRWY7TahzAjlmsQSxnWId0KqZXtxJYg5G16lk/iFaHRM0NLNDdpxLZ1o8xahe8LC2F2behaWtUKP54jIhy26d9S95msxnHUWuNZ9sqcKBFsU7j28m6LZEy0t1Kx1sHelJ38mA9xn/75o/G9YXy52W/e1zm3WYId7c3Ivnh4RFNIyB29GjM8xxCuLu7Qycmsk6gdIsd7vf7ZVm0r&#43;EL54qDCjcYNaBa68bLMufM4zigdct2ZhcEocAkpYTeUq2x3thlBD3LQxgCcJi83W6//PJL&#43;MbWWQfEowp4HMevvvpqHEecqH2K/afdbi/CX3zxxf39PYq3G75rlNya/VHXHWPEatM0oRuu6xp3iipGCX5vPcbdD38PMTqlfVz8sNvtClw2G18YoLg/it7QeFPmeZmmPZwB773Col5J0JqKaFkSiv1xu4P2xc1qTiAvCHWSpmnWzoza26aYhpBBTx1M7yI6VOcX8oGflVJWKLRwdi2P97Vye56xJmA0TTOKw6xIS5Nis97UMeD3ZUn9OikldCpg8/v9NM9Tk/5VtkIo2U1uIaRhGPTtE/oPW6eYUVXZ2H6/P0LA48NDLcpklLxqzYHmH969e9AAS1JaK8KxWuuZcfJpmnUH3B6iLgEUVKiZKDC73W4P4CrgDvnvJtk8s5vnBcK9SQDoQ&#43;6brZwrLAJENlLlHgEInrZukZASPzzsiJyZyyZ/295kt9sDEyZmTQSh7D6lIgOUiilnIcon0lIa/flpWkAi2jt01DjWDJCidVRuVxFEv/vdP7aMD3owue2SVmLdbI1O51gFoDveleuD54o512nmo3U6dX00oV92NbNTYn2Q4MyyzTqQ45eemdC/GsIWPK3kJdbLnzNbghNze6PO7tE5GyOwfFz/MN7c3DQVp98/lOyIW9eJHFnlqwoz&#43;&#43;e0fvTnNSDZ0BsMAQvd432AbLGwRH8nQKP3Yh2givKZEIgq&#43;2oDf4xRC3BqJawX51teSmNlh4SV9H2QhzlVl2d&#43;X&#43;HC5z/MYql1Y0QaqvJaJHm4o6JZ2F1JReV4f1pA9dSI0zR1Za2Dj4MLo1YWppA5Oo&#43;CpuxydogWBxIPkwlWje6iBhDE5ZTnJeFiqqvi4Kn6zZNZV2ldgkyuLqEPxBTDoDdrFAJrBpiAKtW2NtcyDMPhWki7xOupF8XuxgHywUH4OQrltS6PzpfXEiWXZ0latjQ4B0tGbxYoROFNWGVZPBWTKXhhpqZIjv3hQx06OIvtD7GUfVvQ5nDtmFHGp5OK89oaxXyNMBxWMNWtNQCe4iaIz5Kl2aC9&#43;2L3TMBsg8pVs6eYWO95VzQ3wbnkPbHPnhLFomiE9NTkmPT8FJ14csHKF1grXr3T2j44AUXjqrce1assNlFwR8GW6moDeU77jUg5qS8DdailVXfHqVWeOfniJw9lk5JYFkctXOHIKZUUM3Wesjy35ufZCIF1F0n0Fq4m9y1wAsoprHK4gNB1l3m8b&#43;1ooKkuu7CTXH5Src8RkuRyckJOO0y1KJxV0DeIiyPWolcUm5CnSOrfBx&#43;AABSSgNhVSSAkEGopsqPgo5q2sCOTFFQHItV1WqBPmTyFIerFe0wFQ75ePKOdBIVltbBweZaounhQb5Jo81UviI4qlWuOu8ZdzO89z5eHcPShTwVgL1ApUF0UIcUml5yLqZrUoWx9LzQVNVDVNMuRwSeQGNSTdp3HKITGXwWmWqLiqAXFIE9dDdro64qPlvJcOFK7bw715WVRry0eOV9d9xwGWW7A3H57bpcO9VVJnRp&#43;Uiqe5AO6&#43;4Y0QaJkTV7Ic84L7zUnlQ9VOk1Wd2ne1Z57a72fs6plp&#43;NZBy1QH9VZ9udpzwtJq8t9NkwvGq14tF6&#43;QXali/X7a5xmsa6s54zoLPJHMEB9Lc/XiCNnGJQkqGxNiQ&#43;emuschifOLP0/Pb3IyQzpOoGhhGWddXfULByRY5ekm0Tu2mlbrV&#43;FOBYFSZbu2juT8e2WpIxg1zMXjzTctdeQi9HFjYujiyPs4IT&#43;BRxfvAvehwNczE01Yl75sTZahPpA50Sux50cf1pp7anJ7aNV1uRHzW45hH2PL2rpDDMY1cZLiLi6sHHkm//DOVvzMLc4NgwhRqhc0fA8BIT7fzBwkvccoovRD0MMNNyWhYoBcki8HKCgHnkNhqRcTE4E612DZwvXFMgXF17jPKH1GbbIktb/a90xWuk9ITtfvk9ojDtYEsLsEEr1LTeAvUPVsHTVcmJ7QIc&#43;IRXR1IwLtn8Ry3Rqs63XxphyKI3OcO1h9oUe7eDwe4tz1G7sM0WMi1&#43;e6wmHr950JESssHHjsLkdbjdD8DTrZXakncDaJy1ZLfxhCAVLREvK05Qcuc2IKS7nMqfVGFJmnua0JI7BFwWm1iWc&#43;aLlWZZUtLvTV8Sg9yJqa1/ht6CWUMN0gUtBZnH9h1hQAJIO&#43;uLMmtg5OEEVVfpSn4oNwdLqTIchxKB2sO4WXxpiiDFkbcWf5vSwm6e5SBXPe5e&#43;beSfIOX7W3b6TEuLuT6rRCn6zaujB3CexmG4ubn7YjsMIaWyFefcoOCt9K7g3ozlZEvK&#43;yk5KU9i9CIOhOO1XVGkzB/nwkcxgmprUWhQsBYLP6GZslClvsVrESIuLyiosnUAGjULahywoNOQJE79TvgoBa8iEkMAqowOvDZ5xxjUzKhZAN/697ISvwvifQ5u8X5xOVH6X5ffAuLLorWpOZuRI5CXrllKXmtrHB11/xzFDy0H5qI7ublSrz6JwzCMm81mjDlziMhtlXMySxT2REOM41BQEiKHUHYD31B9Nh8HN2p3P2h23IiFRX2L4oKZsu7k8N&#43;F0FY6DymEIJM&#43;KSzFVGS/EruI7KcChgJfoVScModyLq6xbqflFMWoYwcb1xcfh4ViOR95SonnxeHaqVFpY1&#43;oPhWqV7mUspPix4hr7mThwuKkjCq/qPZU6m0W0iktAF9jBARH/kwEV/cU3anBpHJb21QEv8RAGXc6FBOAlbo9OtdD8DDs1QJT7aOFa0P0EqiszfWagc5nKXuZlzzNaS7UlE01tlQfLCAoZhRFlSdJ&#43;/Ok3l3COw2&#43;g6sqN1gju3qFkGP1DgUVVkq8HAJN00JUmY&#43;V8wpDkFuWvNsvc8p9EF&#43;dU3atFJX8EOMNxQ0pqSv8o54s147&#43;2s0QfFAjs3Bz8U6oxjbJzAFyLsrysBJB4n1hs1nmXeaZwKQgWGGZU16WDG4gSAwEsYum5blIHw7Bb8a4FBbwmjItOKlgbXy7n9Pjfp6mIpoMAdUQOjb9oZkPOQadWzSHein4zDwHT4evc226O4RsjxtUHTdF3TpWlcChEI7kN0uenePaCB7IqzZzNWXtveotXG6BzlYNw0govroyZfaKFN&#43;yxEpISvoxv/vP/lVlHzFw2uxkww9B5NCUi70WqslsVQHQflIrVqrYLQJqCNC30JyImLummLTUuWArnTntSQkPnTpyzXKUk/mnFSrvfyLrb50aLxqqYufqf2rEOw4yE9wjIE&#43;CRSqyaoniKIQY3YiLIouOy9nj4hHNxYpmzQqw89s/r14VhlG222nZTJznabcsszS3Qg6FFRYcqSRrH7W8fJX1h8yBO7K71XCWn0XmpqiR8UbGsUoP9cRIY4NI30i9NqQqcUo5iIvCwbvgcuKc5j2izvZf8UBauNiWPH13eJNz2XufR3Y3KY1pmef9Y0qLfJzsymcP&#43;Tq8j0LshoCE86wUST7GcRuGDRR&#43;c1IycQ5MkYJn7xcpwE/Tfnpc0mIODQxYLP5/AQAA///n/j80lE/HeAAAAABJRU5ErkJggg=="
     data-sizes="auto"
     width="1880" alt="A browser screenshot showing bad alt text for a screenshot of VSCode&#39;s auto complete."
     loading="lazy" />



  </a><figcaption>This alt-text is technically correct, but doesn&rsquo;t help the user understand what to expect if they are going to use VSCode with a screen reader. And yes, I worried a lot about the caption and alt text for an image about a bad alt text.</figcaption></figure>

<p>The Trailhead unit is a training article for people getting to know these basic tools. It assumes the reader hasn&rsquo;t even installed VSCode yet. Nothing about the article tells a blind user what to expect from auto complete. Heck, it doesn&rsquo;t even acknowledge that one of the reasons to use VSCode is that it provides support for screen readers or where to get training on use that mode.</p>
<p>The implicit assumption is that the reader is sighted. Accessibility support is therefore provided as a kindness (or for legal compliance) to an audience that would not actually use this instruction. Nothing about this article directs that segment of the audience to content intended to help them succeed.</p>
<p>In my experience a great deal of material on the web about creating good content suffers from the same faults. The implicit assumption about the audience is that they have no relevant disabilities themselves. Meanwhile, articles that aren&rsquo;t focused on accessibility frequently skip the details required to make the demonstrated technique meet accessibility standards.</p>
<h2 id="why-accessibility-gaps-matter-in-training-docs">Why Accessibility Gaps Matter in Training Docs</h2>
<p>There are two groups of people to whom it matters that training materials be correct: people who need assistive technologies to work, and people who care about people who need those technologies to work. In an ideal world that means everyone.</p>
<p>For people who need screen readers and other assistive technologies, accessibility is much more than a checkbox. These technologies help keep people engaged in the world around us and able to productively contribute to our community.</p>
<p>Just to make the situation ever so much more so worse: we are all training AIs through everything we publish. When our content fails to encourage accessible techniques in everything we are training those machines that accessibility doesn&rsquo;t matter. Doubt that? In November 2025 <a href="https://microsoft.github.io/a11y-llm-eval-report/">Microsoft published a set of test results</a> that found that even the best tools only create passing content 30% of the time. Those LLMs were trained off the same material we use to train humans.</p>
<p>It matters to people; it matters to machines; it matter to people who trust machine.</p>
<p>It matters to everyone.</p>
<h2 id="leveraging-your-empathy">Leveraging Your Empathy</h2>
<p>Content is for people – even if machines <del>steal</del> use it to create <del>money</del> content for others. Creating good content requires thinking about your audience and how they experience your content.</p>
<p>A mistake often embedded in efforts to create universal designs is that advocates will at times argue to create the <em>same</em> experience of all users. When possible, that&rsquo;s great! But that&rsquo;s rarely possible in practice. The experience will never be the <em>same</em> for the person relying on a screen reader to read an alt-text as it is for the person seeing the image. Don&rsquo;t pretend you&rsquo;re doing the impossible. That&rsquo;s not being empathic to different perspectives, it is denying the realities of our world.</p>
<h3 id="empathic-alt-texts">Empathic Alt-Texts</h3>
<p>As one thought experiment let&rsquo;s look at alt-text. They are the first thing most people are taught to worry about when creating accessible content. That training nearly always focuses on describing the image.</p>
<p>I am advocating for a different starting point. When writing alt-texts try to put yourself in the place of someone encountering that text. Consider what they need to know about the image in the context you selected. <em>Think less about the image, and more about its purpose.</em></p>
<p><strong>What <em>meaning</em> is it conveying to a sighted reader?</strong></p>
<p><strong>How can you ensure that <em>meaning</em> is conveyed to all audiences?</strong></p>
<p>If the purpose of the image is to convey a <em>feeling</em>, try to convey that <em>feeling</em>. If it is meant to convey information, provide that information (or better yet put it in the caption for everyone).</p>
<p>Ask yourself what you would want to help yourself understand this content. Is the image there to provide a visual break in necessarily dense content? Okay, then make sure the alt-text gives the user a mental break as well. If the image is silly, tell a joke. You gave you sighted audience a reason to smile, give that to your blind users as well.</p>






<figure >
  
  <a href="/images/posts/empathetic-Accessibility/CowAltText.png" target="_blank" rel="noopener noreferrer">
    
    

    











<noscript>
  <img class="rcf-image" src="/images/posts/empathetic-Accessibility/CowAltText.png" alt="A browser screenshot showing the alt-text from the article header image." loading="lazy" />
</noscript>

<img class="rcf-image lazyload show-if-js"
     data-srcset="/images/posts/empathetic-Accessibility/CowAltText_hu_9d9c08f8582baa76.png 680w, /images/posts/empathetic-Accessibility/CowAltText_hu_4779e08ea883f033.png 850w, /images/posts/empathetic-Accessibility/CowAltText_hu_ba6da80c9f983fe0.png 1020w, /images/posts/empathetic-Accessibility/CowAltText_hu_64c90d46788226ac.png 1360w, /images/posts/empathetic-Accessibility/CowAltText.png 1712w"
     data-src="/images/posts/empathetic-Accessibility/CowAltText.png"
     src="data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAAAsCAIAAAAigicJAAAXz0lEQVR4nLx7CY8jSc4dyThSKdXRx6ztPWB8MGDAgP//77GB/XYWM93TR6mkPCJIGmREqtQ9M32MazenW6PWkcrk8fgegxEfHx/VDlGFq&#43;OTfzzP8XunxO/6TW1fQEQk8odPDvuEgt0PoD3xTxMCERL&#43;0SsXMw&#43;IAqGd5Y&#43;f6DeOyFyXeV6WmblerHExRbuHzw59Mhpem237rvpzRLx8HJp39eovtg&#43;ZKa/O&#43;ysvXN2rbl8FRAop7XZjHnaIqFvstCfMXFgLYxVSMxqkiEOCFBD/gOnUjVDNBxjc8/rJZf1/HvF8Or356cf3796sy9LMdvGBAgqQ6HbX3ag9AK8e&#43;/X4B9ppW6DYxW4ussu&#43;GAq2wMTPQkqv87D5qJ3BzQDN1hb2w25///L13YvXtAufmEtVRLlKYVqqKGAgv89gT2jz&#43;ncc2K8Vuwme0/p2YY/Hj//59//zj7//32WeCDEEy1V3MwkG1sBgcYT&#43;VrBMpg0CGgB0MKDt0ezjz/1M0N3WPYe6mRUaMqDZJWxuQDezpx1uoaDYXtg8o&#43;ZdEi673cA3d3DlsvaNECgBYsAQ/Dz2ir3OouLOxB4c8I0J8RQI3&#43;u8b3HAus7Hh/cf3r9d5hnNaoGCGQQoCASGoEgOfhgDxRjc5NSM6qjbUTj4Xzc6&#43;XlQ7Ev9Vtu32hdbNoACK6h5GgUhEES0P2TOcQfhJdiaD/yL3YH2IH58hpEeKIFIiYVAWJGFKivz5WSKaIiUo1/bN9jI/O81oEXN8/ogNu8SAZHdCitUBrO/eg642ZrBKVyApd3rBhl&#43;S9gzx&#43;1PPUFaAbPy5YHZk6V/s6FcAzrzid0jKnVDwnYGvCC/KDZXEKFiFEOUz63xZFLDImahlYH1qVZ5VVCEFivfjyfPnQLR8I0sujkGu0O3esP1gEokIaBfrDkJ/Y9CD2RHXMtsez86NqlIFYvrQKDBS6LVr5QSUuww81ncYS/llhDS6wGCBk&#43;LJ3t63ogDgSgaMLZM&#43;p3DgEgDqYWEf6uXf3XoCwEuBf&#43;rJkWvAeqV7fkhyKqbWOFqQG&#43;gbHhnoUYAwa0YAByoLUuAgt&#43;O2UZVqgjXilpQEwMsyzrNswgPwzCOYwhBRBwTYryyAnQrN0erlwr1py1bemEnVbwYynKNYIv6lni/y2y9OpnTRRCEBaoQS&#43;cv5myBUu2WY0ALrC/aSFumS7ua54YgFS2VS6m1VrNUsIjcChs1Ug1WvTA4Mnn8EaA2BkRARbQu5/lxPZ/P795/fPf&#43;Y6315ubw6uXLw2E/7ve39y8dlUi3q3cob2XQ3EyorQJfA06nNP1Sek0gKwgNm/Sasjbur/orj1iK1sIwl7BU9PC34hwt6SHHFnJfsZFedABCf3xGBxhp41pKqaVaIohsPJOI1JPPzG7ZYJig7h0GC1svDoEyxscyvX/3yz/&#43;88cff3rz4eNRRW9u9q9fvvjhTz/89W9/e/HqhxgjXZe8TQFclA01VgqdsF6Y0rU/vARLc1UwlzFeKQBPYv20CgCzbL6xGOrE198SQRVktjz6gg9akeuuVUVWkIa&#43;/VubPOmc/HtzJAqXMk9lelxLpRCdAllaUrDDz27hK0rarhoYUawOiLjCJHMMyjxNP7999/Obd9O8ECIzo8LhcAghDDmlaHWkGxj75W4ktYuPlg2w6ZwrfMGLETdC5GXVlKNs&#43;CTM3QHX1mMWZq4Myl56HedY/EcDCKKd4qsGE1XuEGRm6MmmTQOKe74ZzIxx4c/f6IC6LvPx/en9z2upGAdKOcQcYrI/IRQD78iS2ShFSNFQ3ZJWSi1n4IUQquLpuJyOD/M8C0tjQHa1RPtxvL/Z74eYDb9My6vbwBC/86aL9NMrkaxPqg1btCq37OyKQJNIrVVE2hfNRCz&#43;zyvz&#43;4u1GgQVCUVIpCcfEQzRQBX0a52FxkGrKJsDGIWt8JnVmwNqLSIaY8w5p5QcxL/DBbGW9XT8&#43;OGXn&#43;dlwZAp5ZSHmIcQkyUEUUw5j4c87IaUhsF&#43;JILwclyOb&#43;p0ZK7TKm8f1n&#43;8PT0&#43;nhAgGZuy&#43;hFjvNnvDrs8BAxawcCNFDeKih0Q/GJ7gZYmuTyUdRNjdpMeydIPe5MV9pWlQQNeKsqn9Kr/M5rfmbRC9fzwxIWNK38NMRpJMz1tjvfqR9Aqll&#43;5YECU0Lj31hZA3foJiFcEGD4BKX&#43;IhflxWt4/nE7nE6DlUUoxxthOGELMu33a36dhv98f7m5v96OSlunDLw9vf5weP8zLejzz22P95cTnVQOFGDpB7DoZVLmAVsNOJIoZQzZN0RC5dwegX5WiGJgYt4Um9AwxLIrZ/nBlQ3VHq8Aq17Z2yPzcB82XYnEqhbWKgZfHrnNcV8shfLFP5xlg1&#43;U/p6gSjLm0fotBEHGDINM/Sh1HsaVxd699Xp/6ZxaEqfUJrAjDWvVxkeNUACAFTGYclWpBmGKIw4j5IQz7u7t7lUr4Aur67t2HNz&#43;9e3z4OC3rwyQPM5wLsFII0dRyCCEabpRS1mWu88lyuMyAGIdDGm8xj0jJg1x7APv/zDiGNBbmhmOG&#43;WgwUrjUWkpdiyWC3WxKfEV6WmN0ozSfmdPqc6BKVUhoqbh6NprED5gJYzTd/gUPaDUsYy8DGIO6CndFSoaM7MmgaAFmCeCxFzbiRuj&#43;79faW5AZKRMli5do6saKGlSB7mTDXCkrq2iOFEqVsGA81TKnIMFgUN5/PL59f/7wME9LmYouTAzBhZQFaFSUrJXreZrPj8c51qgLr7OqpvEGpGaDKtQuAze0aHXWHaAe5oAGNeygW1lXlrXae6SWubVYXbhkwKUx9esk8LJvdwuMVZGkdZRga6V0ef6bTjDLBftJQx6r&#43;ejNEmoKXhiIG9LgU5ergVMDHwEtanVy61BauDiKkbsnXnhIr1rmBhTB6o2r0qv8Cswn4HckWpaA&#43;PDwcJzKcZZp0dJKY4N3b1hqb8lzWebp8eMjngZiixZzLVt&#43;5B1hMkC1rKVA1NpkfEF55RZnIsAuRC5A7&#43;jtuNzouV5QCH6TgVxedKKCIcKYO4mMAVME&#43;mJLyEPau4MeHBbn0dslDKb5vTi3lks/UYN3U7Ngd2wqyaEy9GLQHKirZSUljN3N2&#43;2YJEKr5NF&#43;kdlFo988w7IeHx6krIR0Os3Tyiujd&#43;u6e90wpJu&#43;dYqwno8PodB&#43;wHFneGZJJhXFslqELD1J&#43;nKJg7Wbn/2xlWS7Nd0uPUa7jxBoSEbJCL&#43;1P2mixkwE8akr3ivm1xlLu6IibjVswtlNL56hTSE7IU69bd3lWxFZxIGLmj/Au2t&#43;AaJZNUE0gOpiuHcBvKlptL1UrIWrk5fo9UpElnVV0dNcliJVUVzWIEAkq7/gRNM4hoWqpct0eoQSSomscHMIo1U8tfjxQuaNELM8tWUbxx/nOyxbIrVuXlOgMZB3cjBFiqHB17ceT9TjuxtwnlyRep/EQAkxejEl1QiXfhGGrs201YMcfJFOm3JXFyBWfk2IE5kO9yJci5Qi1SxtJ2FxaaJQWNe6yRbj3pbDRGGtvBQpbqILk4&#43;hqYTQ1NkuhzHBLsoQNAUSwXmpMdYQIs6TxCNwkqBA0fvZGAMET/JStVThK4LfMQl6ke2ZwmQ8QeU7bflHD9ykOVtOIoXWN/fFD&#43;ikyJc11EuuXXw39JOS8JoMGIh2wd5yGh5bjKG2jph3/VRKNX&#43;YIdhrGAEzVDONfaCwsCNejNQb/R6bKYZxN9yMaUghR3p1u/vTi/H1i32KeWWorOe5TssJacoPK&#43;1XHG5D3MUYh0T7HCihiK4LnxeuJrO1lWZxVqre4IwEPZbE&#43;JUww6/7P89&#43;XHKwM/mtQ6jdMZ3Gdl1z&#43;dTGRS/Sxolv95A3QIyH9KYkkgnfaCGuoEvhtXC18mIULXiqM2tZGYyQSApIuygK2xpZl1ZDDvc3491oNr0/DC9u93c3BwgRV15Oy/uP83EqojqO5/3tPN6c9uNhSAlyyGNMQ1TAuvDjcZ6XFdCw3rEb1SVECH7JzpN2I8/z4qLgX2//ZkPWJ/BRw3d7BcFfMRs5Sihs9Ac8M8zWTdZL6&#43;SiBc&#43;kyERDgBSi34C7hozDxxgrs69lWNGPBIkg&#43;mJ0REggg8IuAOQEFLzNK00ZIVJl0&#43;XrutSoiWKtPM0lxDUkXVjfPS5/f/Pw8XFVwP04v5zrq1JCnTUFJoR5wNt9TFmWOj8&#43;fHicqjC5riAyTZ6TF2BgiwunHvOyVK6/0QF97gMvvcOtE6qssloRdvD1mYvs78glL7ThElwipPUwvWA4IqlGxaixq7W2NKgonht9KRU0AxyIUwDBkAIego4RhhzikDENleVxWh/mskprzitznaY5GiUYIiJX&#43;XBcDjf7kNPxvBxPy/G8FNbTHFcrPHWd5yEac7q73YPq4QBlLeuynM/TXCzAKET7az&#43;YhhzIeAD7PcZ5WZnlXzFA82sP9AIbPxk78JUE7AubPrTiUW850UDBDO350V3AzogCmfzzCtF1QF9M91XWxsRBxRe0ZI9wAETRxW40&#43;MqNIcqIkoiJYMcKRR8UKmCIwbvK6Cu1KsyPawFaQsR9wF2E17cZQd4fl3kxyrWWejpPu0QB4DQXwPCisFX&#43;Urz9YATMw8bqzBwppxDD1ksK67pWh6B/uQcsybhJpA1YGvh0RmoJocpt/sLSojWbWjK0Eul9WG&#43;pCmS4OMC51Tbb1DtT3nk0kgGckE14rVoUJzGhdSIdI1ZHw1vmfaZEqgMNQ&#43;K8o5h82cUwm0BKKUWEQlzXlQiHgH/70/7&#43;NqWf8af3hvK18lpo552ApZigPE9zSEkQdwMpkNWhatTU/FBwZZLoXkacl7K4n/4dGXB9tBXyjK2yAm9zK41lPq1zbtLa67PXj96IN18a8W9&#43;ckxqDovBZU6AUp3lodXbpfLE5oBFUJzCLtHOOCDcBIx5SCm&#43;SOF&#43;fxP3e0o5pgwCx&#43;Pjh4fjaV0FMIIcz8u08rjLrw952IW1ymmp68NSq64YQIw&#43;zUWPUwWah0GHw3DY52EIZWVDqtUKkmlJH7ZA50VSa1Pt/ya7d81iMG1BnU2PWRlwHvyke72BBilg1Mt6k8Vz4X618qSfjfRT7DSVEGKknAlRS1GjQt6CX6sUdg7WiRVUxSKoFHbjeHN/m3a725BhyBoCYxj3&#43;5QSBvzlcTqVFUCj4RHHqBRDFd2N6dXL8TjXtciycgw0DHk/DuNuGPe7vBus6oach7QjA7G68vm8nk9rrWpR1Xi/avBl/z806vb9tteNa25zHNAbsAiCbVDJIqE6FokSilmUnuqx5bB0OgouarSKTyGoUapABuiVODhfjSCJlAFWUePvgm0F14pHDPtdvLnZ3b26/&#43;Gv/&#43;XPf/7hcHujFOZlef/weFpYhUuFaVnPS5mKhLZg5iI5ulQb9uPL/VjAGNTH4xJDuL8/3N/d7HJyN2RjPjkl05AWVJyqsk6n1QCtYPDecRtC8r7r1xbUn&#43;swpeNo7gJdF&#43;7wEjas6Q1q/yhtLWj/jLZmQ900IxmhxNhoa5&#43;KMFjiutTV04jZqL79BwLUeomEuAv4Yhde3&#43;bXr27&#43;8tcf/vwff/nzX/40jqOKnB/PVhwfzlNZP7yffnr74TSdmTWGPOR4s885h90uDbs87MchhFca5kVznono/na8uxuHnEZLgGyOyhEJ2USggX6pms/rNJelMLJlUsKgGHxM4LknBX/P&#43;lu33J6zBYZJk4g4BGgcR7a8IH0a7mzKgF00FGkyuM8NRp/E8laEya5pXs&#43;TsQpmP7k3ISuDtpkstXzfJ7rb0e0u3B3ii7vh7n7c7XNKyEXzEO5u9sZq5vnhdJ6WmUh3O9rvws0h7g8pRcqDZ4Oz1RDCfpfFkT0SSC0aMYSUB0qWA2gBYPnsqzJWWgaMRYoyC3pYrFVMD8hXhqr7avpGlS4l43tGE7Gtn8ClY711YbF3hLYXWS8LNRi34TUCTYqJLAMal20rF5komzMii5Yqc5F5bYtO2kgOaRcE7QKGCLshhhQr4FrLPJ&#43;n08NppCUGKcJWtyVFjNHE8N3tbjzkGGk35CGnnLzjnKhKfTye16LnqUyntS4VQFdhQkkJAXfk/TWudZnXZSli1o7zCgwx5l2GsK7FO6VQLUu5EaQvWL&#43;19liBhTyhUb13mSMOyUv6t/gAsVFMt&#43;ynoHf9/U8Gi69fB9hH/92r3q3PaXovyLOnsq7e&#43;fExSt1WafHi/l2ilAIjnVnxcclv3uchrMsUAnkgU0oZKQ5jGg&#43;ZjbLSsEt5MKHX&#43;gVKMM3rPE&#43;nc5lnb74KmG8y7XLYj3YCDJZ6FgeruacyVqtDgUI&#43;3OahmsqzUoAQtkXaL5uurSeXCgtrEdLWcgfYD31e&#43;hus3x&#43;3&#43;ZnO63//o795HRf99vk8Xty81Tdp9CaGx35r88RA2UmqgVUVX5YythUDnk9TG1mgEA83&#43;93hMFU1hr7UkGLyiEPvyPqwNNVV1sKlWK32gTjMgQ5D3A8pG723Eq4mdNVHe33tgAWBcorqA74ibALTkwZRvuoAj10SX964aiWAdzW&#43;czQR4Wpa9TsP/J3nXtrxsiwXfApL23i4J1wIFKPTLYC5MvQWNKjImKMIqgsvQH53qhhPRfA0mYVjSmvRPBeKIec0jjnFmAeTfoOhv3Dh6uKCi06n4otfIpzarMe6lnkuywIikWLCZBnMZS3LvK4rM8cYmkj7FiWMl90NupEUHzRBhtig4ItHn5Hxbtqzbs6AngHtItsoHG0Daw7/1MRb46xWzU2WNloEt2ceD5hzBgxV4Hwup2Up1cQpUcwZa1U6ITjTLzd62OM45v3NLnhzta58/Lh8/LBMx0KPJQ3xdt4dboZAAewXy/k8T&#43;cqjGnIeRwUcJmWeVq8BaQxhGpC&#43;yvrAT735xBkKIRtsjoSVNIoqAkwQW97/97RcK61H9q8yfPukPHpJSei0hddW3u1Vf22kMBPuY4tQpeKDzOnU9lV025V0Mu4rt59JbI6UrkPAVXHFalKSLvdsMtG8mvQdY3wqNN5rrXQUhaD&#43;BJjMs6UsDAsFvIcq1EeIFrXtazzOpfKOuShlqJfa0X0TWTBd5voxt2pt9G&#43;6bh0/BsmPPsOGbjS2W3&#43;2CHIxz7dD1TVLNjWfWlb&#43;Cc6V4GHOUXxJh20SYOmli1VipGUGCml2EbUjIdVxQpYfe9NRZ8SEohcC2upcC7CmpMMIw0hC2bFldXE8CorBTJG0ybdiqxrqcVZ0Bc9gNsuqEA92fs06rYt55uN2bczPD8EGbOKFH0pxuDDE9L3CRjtCSaJBKWNqfnOCJ/UpGBPpoXP8&#43;IDh67vUsxtCBS1j6iIolJETZbuEFh54nOti8hacak6V1PafbFO0ZslZlQ2xpIEctVafWnUiimqj5hf66&#43;vBGSfTmTwLWPQlKn3lEA9Lb5eQHSbzXKy9/wQlHfD7d3d/YsXLvy5DeoEHzUdson9NUlM2phD9PFTXx9Ub5xS5b79oX1&#43;MCnbti/1ZM8xjEM85DzmATWcJ55O81SBNSCFKkgpJx9xShFTDilH30tASoHSSBFQ1r5Dz5cHduMACrd39/vDTUrp1/h9tWkS&#43;rg09nEQ3ML/sr3w68fTouOWMs8LQS9fvv6f/&#43;t/s8jx44c269rIFmEb97Va6o0wjAFzCsmUiPrQsYrgxQFElA2/fc4P1cAnmrfMByGM0XJMBKcKcZWBjblmE8fkU7cMRkwlGeWhECJQEMVaxThPrR6BmzF9CvHu9u6//8f/uL29p0/JfBNfPj&#43;KrMiMIsFQ0iXeZS9AG62IfVvVtxiqbe97TtP38/7zx3&#43;8&#43;fmf7355uyyzql79RJ9oMoLoTNM1AbYdh36bfWKnLYbhphtaZHm3rN&#43;e60cDLxZwGaE&#43;xUS&#43;aYC63XwQqCmsTsG89dsm5fpWtE4ira4Ow3D/4uWLV6/Hce9ToW33oP26FYmqrFQYS0O0bZy/NzRbRznimDGnr23g1j7h4/uE2/Lvc/ohHm5u8zD81//21y9QuotguWTudZpf5&#43;SnWzCetHffmAd97Plqu/sWXJfdGJ8FZN&#43;9fb0d3Gd63X8xpt&#43;ahmvzeU/bofCqGvd1KoJv7QW1jwXn5N/YuvieI/peqkwU/g2r2894XFbxrl/c9gkH41eKWUA&#43;b8/0VkBzRgzfZk&#43;6bIp6/o3C/y8AAP//RqqqvaWz6y0AAAAASUVORK5CYII="
     data-sizes="auto"
     width="1712" alt="A browser screenshot showing the alt-text from the article header image."
     loading="lazy" />



  </a><figcaption>This is the alt-text I chose for the header image, note that it includes both a description of the image content and the emotion the image is hoping to evoke in the reader.</figcaption></figure>

<p>When I wrote the alt-text for the header image, I wanted to help make it clear why that image was part of this article. I tried to include not only some sense of the image content but also the emotional reasoning for selecting it. Hopefully for those who saw the image all that was clear right away but for everyone else I still wanted to provide that detail.</p>
<h2 id="what-this-doesnt-cover">What This Doesn&rsquo;t Cover</h2>
<p>No one approach addresses all needs in all contexts. We cannot close every gap.</p>
<p>There are places where no matter how much you care, it&rsquo;s really hard to create proper parallels. Descriptive audio is really hard to create and should be done by experts when possible. Creating teaching materials or tests where students need to struggle with a piece of art to find details and understand its meaning is even harder. That does not mean we don&rsquo;t try.</p>
<p>We should do our best and strive to improve over time. If we pretend everyone can provide all the accommodations equally well – or that AI will magically free us from this challenge – we hold back the push for further improvements. Many universities are doing this right now when they tell their professors to handle all accessibility needs for online classes without providing the needed resources. Even worse, they often tell those professors to remove valuable content if the office staff don&rsquo;t know how to make it accessible. Harming everyone is not the right way to make progress.</p>
<p>Do what you can. Admit when you cannot be as good as you would like. And use that admission to get better, push for more resources, and support technical improvements.</p>
<h2 id="helping-out">Helping Out</h2>
<p>If you&rsquo;re interested in helping me review Salesforce&rsquo;s Trailhead content, or just find something that needs review and improvement, contact me through <a href="https://www.salesforce.com/trailblazer/acrosman">the Trailblazer Community</a>, <a href="https://www.linkedin.com/company/ohanaslack/">Ohana Slack</a>, or <a href="https://www.linkedin.com/in/aaron-crosman/">LinkedIn</a>.</p>
<p>Accessibility bugs can be reported directly through the Share Your Feedback button at the bottom right of each unit. I&rsquo;m happy to help get attention of those issues if your alert me to what you encountered. If you think you found something more subtle, where it works in the technical sense but is done poorly, please reach out and I&rsquo;ll help your feedback reach the right people.</p>
<p>For what it&rsquo;s worth I also know this blog is less than perfect in this regard. I have been improving it when I notice issues, but please feel free to send me suggestions about what is causing the most pain so I can prioritize fixing it.</p>
]]></content:encoded> </item> <item>
      <title>LWC Links to Custom Metadata</title>
      <link>https://spinningcode.org/2025/lwc-link-to-metadata/</link>
      <pubDate>
        Tue, 18 Nov 2025 00:00:00 +0000
      </pubDate> <guid isPermaLink="false">https://spinningcode.org/2025/lwc-link-to-metadata/</guid>  <description>How to navigate to Custom Metadata records in a Salesforce LWC, including edit and manage records links.</description> <content:encoded><![CDATA[<p>Now and then I have to work something out that requires a lot of searching around to find all the pieces. When that happens I often write up the instructions on the theory that other people might benefit from the complete answer.</p>
<p>This time I wanted to work out how to create a buttons in a Salesforce Lightning Web Component that allow a user to manage the records.</p>
<p>There are two links that matter in lots of use cases:</p>
<ol>
<li>The edit link for a specific record</li>
<li>The manage records link for custom metadata type to add or delete records</li>
</ol>
<p>The link to a specific record is fairly easy. It&rsquo;s just a simple row action and well supported. The manage records link on the other hand took a bit more research.</p>
<h2 id="custom-metadata-type">Custom Metadata Type</h2>
<p>Obviously, the first thing you need is to <a href="https://trailhead.salesforce.com/content/learn/modules/custom_metadata_types_dec">create your custom metadata type</a>. Exactly what you create isn&rsquo;t important here, the one I was working with was the <a href="https://github.com/acrosman/SalesforceInvalidateEmail/tree/40af241eb9ceaf9f3a018e743cb3ad248235481f/force-app/main/default/objects/Email_Invalidator_Fields__mdt">field list type</a> from my <a href="/2025/salesforce-email-invalidator-tool/">email invalidation tool</a>.</p>
<p>For the purposes of this discussion I will use <code>Your_Metadata_Type__mdt</code> as the placeholder name. Swap in whatever your actual metadata type is called.</p>
<h2 id="the-lwc">The LWC</h2>
<p>Next we create our LWC to help us manage the records. The common use case here is when you&rsquo;re creating a settings page for a custom app or tool. Hopefully one day we&rsquo;ll be able to make these settings pages, but for the foreseeable future they generally need to be a custom app, with a Lightning Record Page, populated with one or more LWCs. Putting all those pieces together is beyond the scope of this article – I am telling you because that is the context of my assumptions.</p>
<p>A basic metadata setting for the LWC should look something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#75715e">&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;LightningComponentBundle</span> <span style="color:#a6e22e">xmlns=</span><span style="color:#e6db74">&#34;http://soap.sforce.com/2006/04/metadata&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;apiVersion&gt;</span>64.0<span style="color:#f92672">&lt;/apiVersion&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;isExposed&gt;</span>true<span style="color:#f92672">&lt;/isExposed&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;targets&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;target&gt;</span>lightning__AppPage<span style="color:#f92672">&lt;/target&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/targets&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/LightningComponentBundle&gt;</span>
</span></span></code></pre></div><p>We will also need a basic HTML file for this LWC (obviously yours will probably have more elements – mine does):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-html" data-lang="html"><span style="display:flex;"><span>&lt;<span style="color:#f92672">template</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">lightning-card</span> <span style="color:#a6e22e">title</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Metadata Configuration&#34;</span> <span style="color:#a6e22e">icon-name</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;standard:custom_metadata_type&#34;</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">slot</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;actions&#34;</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">lightning-button</span> <span style="color:#a6e22e">variant</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;brand&#34;</span> <span style="color:#a6e22e">label</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Manage&#34;</span> <span style="color:#a6e22e">title</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Manage Custom Metadata&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">onclick</span><span style="color:#f92672">=</span><span style="color:#e6db74">{handleManageRecords}</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">lightning-button</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;slds-m-around_medium&#34;</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">template</span> <span style="color:#a6e22e">if:true</span><span style="color:#f92672">=</span><span style="color:#e6db74">{hasData}</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">lightning-datatable</span> <span style="color:#a6e22e">key-field</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Id&#34;</span> <span style="color:#a6e22e">data</span><span style="color:#f92672">=</span><span style="color:#e6db74">{metadataRows}</span> <span style="color:#a6e22e">columns</span><span style="color:#f92672">=</span><span style="color:#e6db74">{columns}</span> <span style="color:#a6e22e">hide-checkbox-column</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">onrowaction</span><span style="color:#f92672">=</span><span style="color:#e6db74">{handleRowAction}</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;/<span style="color:#f92672">lightning-datatable</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">template</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;/<span style="color:#f92672">lightning-card</span>&gt;
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">template</span>&gt;
</span></span></code></pre></div><p>In this example I put the manage button at the top and a simple <a href="https://developer.salesforce.com/docs/platform/lightning-component-reference/guide/lightning-datatable.html">lightning datatable</a> which will hold one row per metadata record. The button has an <code>onclick</code> action that will call <code>handleManageRecords</code> in the LWC&rsquo;s JavaScript controller. The rows will each have a row action block that triggers <code>handleRowAction</code>.</p>
<h3 id="the-javascript-controller">The JavaScript Controller</h3>
<p>Things start to get more interesting when we get to the actual JavaScript. In fact most of the important work is here.</p>
<p>The JavaScript starts by loading the required modules. Three from Salesforce, and two from our custom Apex Controller (coming up next).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-JavaScript" data-lang="JavaScript"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">LightningElement</span>, <span style="color:#a6e22e">wire</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;lwc&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">NavigationMixin</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;lightning/navigation&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">getMetaDataRecords</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@salesforce/apex/MyMetadataController.getMyMetadataRecords&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">getMetadataPrefix</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@salesforce/apex/MyMetadataController.getCustomMetadataObjectPrefix&#39;</span>;
</span></span></code></pre></div><p>Datatables need to know what columns they are displaying – and it&rsquo;s best to put those in a variable for easy maintenance:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-JavaScript" data-lang="JavaScript"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">COLUMNS</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>  {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">label</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Label&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">fieldName</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Label&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;text&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">sortable</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>  {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">label</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Developer Name&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">fieldName</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;DeveloperName&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;text&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">sortable</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// Plus whatever other fields you need.
</span></span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><p>To make the Edit links work for each row we need to extend the provide <code>NavigationMixIn</code> class we imported above.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-JavaScript" data-lang="JavaScript"><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MyMetadataLWC</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">NavigationMixin</span>(<span style="color:#a6e22e">LightningElement</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">columns</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">COLUMNS</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">metadataRows</span> <span style="color:#f92672">=</span> [];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// This does the work of actually loading the Custom Metadata.
</span></span></span><span style="display:flex;"><span>  <span style="color:#960050;background-color:#1e0010">@</span><span style="color:#a6e22e">wire</span>(<span style="color:#a6e22e">getMyCustomMetadata</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">wiredMyCustomMetadata</span>({ <span style="color:#a6e22e">error</span>, <span style="color:#a6e22e">data</span> }) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">data</span>) {
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">// Convert map to list
</span></span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">recordList</span> <span style="color:#f92672">=</span> [];
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">recordNames</span> <span style="color:#f92672">=</span> Object.<span style="color:#a6e22e">getOwnPropertyNames</span>(<span style="color:#a6e22e">data</span>);
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> <span style="color:#f92672">&lt;</span> <span style="color:#a6e22e">recordNames</span>.<span style="color:#a6e22e">length</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">recordList</span>.<span style="color:#a6e22e">push</span>(<span style="color:#a6e22e">data</span>[<span style="color:#a6e22e">recordNames</span>[<span style="color:#a6e22e">i</span>]]);
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">metadataRows</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">recordList</span>;
</span></span><span style="display:flex;"><span>    } <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">error</span>) {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">metadataRows</span> <span style="color:#f92672">=</span> [];
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// Simple helper method to make the templates a little more stable.
</span></span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">get</span> <span style="color:#a6e22e">hasData</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">metadataRows</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">metadataRows</span>.<span style="color:#a6e22e">length</span> <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// The edit action defined here is the really important part for navigating
</span></span></span><span style="display:flex;"><span>  <span style="color:#75715e">// directly to the record you want to update.
</span></span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">handleRowAction</span>(<span style="color:#a6e22e">event</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">actionName</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">detail</span>.<span style="color:#a6e22e">action</span>.<span style="color:#a6e22e">name</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">row</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">detail</span>.<span style="color:#a6e22e">row</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">actionName</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;edit&#39;</span>) {
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">// Navigate to the record page for editing
</span></span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">this</span>[<span style="color:#a6e22e">NavigationMixin</span>.<span style="color:#a6e22e">Navigate</span>]({
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;standard__recordPage&#39;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">attributes</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">recordId</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">row</span>.<span style="color:#a6e22e">Id</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">objectApiName</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Your_Metadata_Type__mdt&#39;</span>,
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">actionName</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;edit&#39;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      });
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">async</span> <span style="color:#a6e22e">handleManageRecords</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">metadataPrefix</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">getMetadataPrefix</span>();
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Navigate to the Custom Metadata Type management page
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">this</span>[<span style="color:#a6e22e">NavigationMixin</span>.<span style="color:#a6e22e">Navigate</span>]({
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;standard__webPage&#39;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">// I&#39;ll explain this bit below.
</span></span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">attributes</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">url</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;/lightning/setup/CustomMetadata/page?address=%2F&#39;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">metadataPrefix</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#39;%3Fsetupid%3DCustomMetadata&#39;</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    });
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Currently the NavigationMixIn only provides direct links to Custom Metadata Records, not the manage records page (or the create record form). For the edit links that&rsquo;s just fine: we use the NavigationMixIn&rsquo;s behaviors and off we go. But to get to the Manage Records page we need to create the link itself.</p>
<p>If you go to the page Manage Records page for your Custom Metadata type you&rsquo;ll see the URL is in the form:
<code>/lightning/setup/CustomMetadata/page?address=%2Fm01%3Fsetupid%3DCustomMetadata</code></p>
<p>Salesforce knows which Custom Metadata Type you&rsquo;re talking about from this bit: <code>%2Fm01%3F</code></p>
<p>In URLs anything that starts with <code>%</code> should be followed by a two digit hex code. These are URL Encoded characters (<a href="https://www.w3schools.com/tags/ref_urlencode.ASP">w3Schools has a complete reference</a> if you want it). <code>%2F</code> is <code>/</code> and <code>%3F</code> at the end is <code>?</code>. Between them is the custom metadata type&rsquo;s prefix, in this case <code>m01</code>. This value is Metadata Type and Org Specific: you cannot safely hard-code it. We have to get that from the describe of the type, and that means diving into Apex.</p>
<h3 id="the-apex-controller">The Apex Controller</h3>
<p>To bolt this all together we need two tiny pieces of Apex. The first method will give us the list of records to display, the second gives us that prefix we need for the URL.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-Java" data-lang="Java"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> with sharing <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MyMetadataController</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// This gets a list of all the metadata records we plan to display. Pretty standard for LWCs.</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">@AuraEnabled</span>(cacheable<span style="color:#f92672">=</span><span style="color:#66d9ef">true</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> Map<span style="color:#f92672">&lt;</span>String, Your_Metadata_Type__mdt<span style="color:#f92672">&gt;</span> <span style="color:#a6e22e">getMyMetadataRecords</span>() {
</span></span><span style="display:flex;"><span>    Map<span style="color:#f92672">&lt;</span>String, Your_Metadata_Type__mdt<span style="color:#f92672">&gt;</span> records <span style="color:#f92672">=</span> Your_Metadata_Type__mdt.<span style="color:#a6e22e">getAll</span>();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> records;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// This provides us with the key prefix for the custom type – needed for the link.</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">@AuraEnabled</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> string <span style="color:#a6e22e">getCustomMetadataObjectPrefix</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> Your_Metadata_Type__mdt.<span style="color:#a6e22e">sobjecttype</span>.<span style="color:#a6e22e">getDescribe</span>()
</span></span><span style="display:flex;"><span>      .<span style="color:#a6e22e">getKeyPrefix</span>();
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Of course you&rsquo;ll also need test coverage for these functions as well. Everything you need is kicking around in the working example.</p>
<h2 id="working-example">Working Example</h2>
<p>My working example is embedded in the Email Invalidation Tool I built a few months ago and am slowing improving.</p>
<ul>
<li>The <a href="https://github.com/acrosman/SalesforceInvalidateEmail/tree/40af241eb9ceaf9f3a018e743cb3ad248235481f/force-app/main/default/lwc/invalidateEmailFieldsList">complete LWC is here</a>.</li>
<li>The <a href="https://github.com/acrosman/SalesforceInvalidateEmail/blob/40af241eb9ceaf9f3a018e743cb3ad248235481f/force-app/main/default/classes/InvalidateEmailFieldsController.cls">Apex Controller is here</a></li>
</ul>
]]></content:encoded> </item> <item>
      <title>Dreamforce 2025 Communications Resources</title>
      <link>https://spinningcode.org/df25/</link>
      <pubDate>
        Sat, 04 Oct 2025 17:39:46 +0000
      </pubDate> <guid
        isPermaLink="false">http://spinningcode.org/?page_id=3</guid>  <description>Resources from my 2025 Dreamforce presentation.</description> <content:encoded><![CDATA[<h2 id="what-is-this-material">What Is This Material?</h2>
<p>This is supporting content for <a href="https://reg.salesforce.com/flow/plus/df25/sessioncatalog/page/catalog/session/1748986623554001htMZ">my talk at Dreamforce 2025</a>. My talk references many of the resources listed here. This page also includes other content I hope people find useful.</p>
<h2 id="writing-references">Writing References</h2>






<figure >
  
  <a href="/wp-content/uploads/2024/05/IMG_4303.jpg" target="_blank" rel="noopener noreferrer">
    
    

    











<noscript>
  <img class="rcf-image" src="/wp-content/uploads/2024/05/IMG_4303.jpg" alt="A pen on a table next to a leather covered note book with my initials in gold leaf and a Salesforce MVP stamp." loading="lazy" />
</noscript>

<img class="rcf-image lazyload show-if-js"
     data-srcset="/wp-content/uploads/2024/05/IMG_4303_hu_66bb7480d3411e06.jpg 680w, /wp-content/uploads/2024/05/IMG_4303_hu_fe79d1044f296c19.jpg 850w, /wp-content/uploads/2024/05/IMG_4303_hu_7ebe17e0edd73906.jpg 1020w, /wp-content/uploads/2024/05/IMG_4303_hu_620dfde36ebf7cb5.jpg 1360w, /wp-content/uploads/2024/05/IMG_4303_hu_e7eee8928dee66d4.jpg 2040w, /wp-content/uploads/2024/05/IMG_4303.jpg 3714w"
     data-src="/wp-content/uploads/2024/05/IMG_4303.jpg"
     src="data:image/jpeg;base64,/9j/2wCEAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDIBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAFsAgAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5&#43;gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4&#43;Tl5ufo6ery8/T19vf4&#43;fr/2gAMAwEAAhEDEQA/APRqKKK5TQKKKKACiiigAMnFZet67Bolj9qmyx6BRV89685&#43;JV1iOG3B96BlofFe08zD2r4&#43;talj8StFupAkjmLP9&#43;vETyajkjOc1qaeyPokeLNDVwft0XPvVqPxRo0n3L6H/vqvmzcfU0eYw6MR&#43;NMPZH08msabN927hP0aoNU0ax1602yorDs4Oa&#43;aFup0PyzOPo1e1fC/Vmu9EeGeVmKPxk0B7M5LxP4SuNIlLmAvB/C6jpXP28aSXESK2NvqK&#43;ibi3hvIXimQOjfwmvOfEXgtbSY3FpGfLPp2rSlVOY7vNSVFRmuU0JaKizRk0AS1Gc1T1PUxptm00hUVHpWpjUrRZBWoF&#43;vIPiLdebq3l/3BXrTnapPpXhvi65&#43;0a3cv6NtoHSMAGrO&#43;Ly8VRozTOklkMafw1ESmKjJJNJQAV6h8KJv9fH/ALQry&#43;vQvhZL5eryR/3hmgzqHtoqQqs0XlyAEVEOgpaDEqcVHRUnakBHRJR3oNMDhPiHdEWKQA4zzWn4LQposRP92l8S&#43;HP7TjNw7/Ko&#43;7WjodqLXTY4x2FaVDMu6hJ5VhM/91Ca8A1SXzbuaR/4mNe1eKLn7PoVw3qu2vEbo7iT61ma0ihRil71JQdJWPWgCpJKKAACuu&#43;Hk3leI4&#43;fvDFcjW34Sl8rxBaNnHzUAfRy/cH0pabAcwRn1FOPWg5ihRRRQIKhncIpYngcmpqxtfn&#43;z6dIV&#43;8xAFCM2WNVuFl0dGTpIQBU1oPLtY/pWdqAxbWFuOoAJrWQfuQPatKn8QKf8M5Px7deXpKx/wB815Lc16L8QZt11BAD91c151c/fNZnTSKtHaigxSYzs4oGRGilKt6GojnNBoBq7pEv2fVbd8/dcGqNSQsVnRvQikB9O6ZP51jC3qoq9XPeFrnz9Ht29UFblBzFWiiimZERzg1h&#43;ITmO3V/uGVd1dAcYrP1KxS8tGRjggbga0pBUMt7mO/1uNYn3JGuOK3&#43;gxXL&#43;FtHltZp5pcnLcZrp58iNiOwoBHlviyUza7Ln&#43;AYrkbkV0&#43;rBjfXLyZDM38QrnbmOg6TLk4NWYb0CNkdN1V5VOelJ5RxxWYy79ohlifgKV&#43;7WaeppTGRSUjQKBwQaB1qXy81kB7d4BufM0WEei12Vea/Dq5P2Hy/7pr0Xd70HMElRVLLUXauoyI&#43;9P4IqPvQKAJAoUcAClwD1oooAp3ek2N5zLAjH3FZsvhHSZP&#43;XVfwreorMDkZfAOly/8ALMrVWX4c2B&#43;5I613FFI1PNLn4aZz5U/5isuX4aX8efLkRq9f/gpMDFZGp4dc&#43;B9Wtz/qd/8Au1WHh&#43;&#43;gf57WQ/hXupAPUVCY0J5UH8KA9ozh/AdncwSS&#43;ZEyJ/tCu/yfWmwoqj5VA&#43;gp1AH/2Q=="
     data-sizes="auto"
     width="3714" alt="A pen on a table next to a leather covered note book with my initials in gold leaf and a Salesforce MVP stamp."
     loading="lazy" />



  </a><figcaption>We all need good writing skills</figcaption></figure>

<p>It&rsquo;s good to have an understanding of what other people expect from us, what the rules of the road are, and how we can break those rules at times.</p>
<p><strong>Elements of Style</strong> aka <em>Strunk and White</em>,</p>
<p>~ William Strunk &amp; E. B. White: The premier guide to the constructs of formal written English. This is your driver&rsquo;s ed manual: the rules other people expect us to follow, but not sufficient for all conditions and settings.</p>
<ul>
<li><a href="https://search.worldcat.org/title/41548201">World Cat</a></li>
<li><a href="https://www.amazon.com/Elements-Style-Fourth-William-Strunk-ebook/dp/B07NPLQD4B">Amazon</a></li>
<li><a href="https://www.barnesandnoble.com/w/the-elements-of-style-william-strunk/1116670762">Barns and Nobles</a></li>
</ul>
<p><strong>Eats, Shoots, and Leaves</strong></p>
<p>~ Lynn Truss: An exploration of how to use the rules effectively. We can bend, twist, and even break formal rules to improve our writing.</p>
<ul>
<li><a href="https://search.worldcat.org/title/55126992">World Cat</a></li>
<li><a href="https://www.amazon.com/dp/1592402038/">Amazon</a></li>
<li><a href="https://www.barnesandnoble.com/w/eats-shoots-leaves-lynne-truss/1100734289">Barns and Nobles</a></li>
</ul>
<p><strong>Blah, Blah, Blah</strong></p>
<p>~ Dan Roam: How to use images to fill the gaps our words leave behind.</p>
<ul>
<li><a href="https://search.worldcat.org/title/858763129">World Cat</a></li>
<li><a href="https://www.amazon.com/Blah-What-When-Words-Dont/dp/1591844592/">Amazon</a></li>
<li><a href="https://www.barnesandnoble.com/w/blah-blah-blah-dan-roam/1100633756?">Barns and Nobles</a></li>
</ul>
<h2 id="speaking-references">Speaking References</h2>






<figure >
  
  <a href="/images/posts/JamesBrownStatue.jpeg" target="_blank" rel="noopener noreferrer">
    
    

    











<noscript>
  <img class="rcf-image" src="/images/posts/JamesBrownStatue.jpeg" alt="A statue of James Brown singing from downtown Augusta, GA" loading="lazy" />
</noscript>

<img class="rcf-image lazyload show-if-js"
     data-srcset="/images/posts/JamesBrownStatue_hu_b7f5894ae1b47a32.jpeg 680w, /images/posts/JamesBrownStatue_hu_e6001ca6a543e290.jpeg 850w, /images/posts/JamesBrownStatue_hu_b70edc25aaeacab2.jpeg 1020w, /images/posts/JamesBrownStatue_hu_5a660e7ecbd982f9.jpeg 1360w, /images/posts/JamesBrownStatue_hu_b84ce7b75262dcbe.jpeg 2040w, /images/posts/JamesBrownStatue.jpeg 2325w"
     data-src="/images/posts/JamesBrownStatue.jpeg"
     src="data:image/jpeg;base64,/9j/2wCEAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDIBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAIAAgAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5&#43;gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4&#43;Tl5ufo6ery8/T19vf4&#43;fr/2gAMAwEAAhEDEQA/APMLwZlFPuI3jCe61Hc/6xe1KonmO5m3BeFFZ/8ALs6qn8UikU7E&#43;StbQdFvNWvljtoyeeWx0rptA8G3esJGZE2Q92Ir1fRNAs9FthHBGFI6tjk0qQsT&#43;7Kvhrw1BotqCRmdh8xroKKK1OYKKKKAIpY65zxBoaapbk4xMn3TXUEfPVaUc9KB0qvsmeNzWklpK8ci7WX9ahHWvSdf0KPUITJGNsy/rXBT2cttKY5EKstcNWl7M&#43;kwuKWI3IB2qyOlQbWP8JqUb9n3T&#43;VZ2Oq6H0U4RuR90/lR5E390/lRYyujn20qTWNTgWxsZCmfmJXivRdG&#43;HdrFKtxdIMjnyx0ruobS3tVxbwxoPRRT69I&#43;VGW9tDbwhY1CInRRT/v/SjrUojoAMUUUYoAP46Mc9Kl8vvRigCLHz9KilG/tVrFReXQBQMfWuM8aTxWMEcgVfNZu/pXeyxV5V8U2lh8hwGK/TigabWxn2&#43;uwlMGNK3LZJZokkTZsb2rySzmubi8SMBtpYV7nZWf2bT4I9ucIDmiwczM37Ncf3h/3zR9nm/vn8q1qMfJRYLs2f7bROiUf2yP&#43;ef515K3i7Uuh2KfpUR8V6n/AHx&#43;VAj1/wDttP4YqP7c/wCmX614&#43;fF2rAHDJ&#43;VUj4z1rzf4PyoA9t/4SD/plR/wkH&#43;xXjcXi/Vn6qn5VZ/4S7UsY2J&#43;VAHrY8QSH/llQfEBHVK8mj8Xamh&#43;4lWR4uvv44kagD08&#43;IX/AOedRnxBJ/crzoeL5f47UfnVmLxbH/y0hIoA7HUvFMlpZvMyDgcV5ZrfiC&#43;1u8SCcYgY9K19f16CeyWKDqxy2e1ccdRWaXY7BWHQ0Abun2ltDKVAGRzXZaPrbpcR2853xOdufSuD05XNwz&#43;arLt9a2FYeWhQ/MrUAeqzaPFOoaKQDPNZ1xp9xb8FMiqFrfyLCmyYk4HerB1u6Tq2&#43;gDzXVbWMa7bxBF2HG6tmTw/ayjhdv0rM1TY&#43;tW8nzdq6qAgqMHNAHKX3hp7eMvG26ueezlW82MpHFek3p/0dqxUgUy7yAaAOeisJcfcNWY9MnP8FdCdqjJwBTl&#43;b7vP0oA5e8h&#43;wJvmHWqouozz2rf1/Sr&#43;9hQW1uzVn23g3VpIxviVD/tGgCstxAxAAyasi28wZqyujTaPKontzLI38QHAqZY&#43;vagDk9VlInZVzhRis8WpkjaTv2rT17EZKj65qraSAxqFz0oAgsp5baVgWPStOHV5FyFNZcsZErn2pLbiT/eFAHe2UWotbJLEzlW9Ktx3&#43;o2xw8Rf6itLwnJ5uioP7tbmxD1UflQBwlxe28kiTOoCrV/S7mMyNmf5X&#43;4M1yd1n7EvXpWXp73899D5e4RxtxWtUKZ6jc/6is8ffrVEZlsV9WFY8Fnc&#43;ZKHDYz8tZAZ/iBbiaCNLckDvitjw7bXUccPyj/a3Vh6pbailwnkQyOF71Y1DxBqej6Esgtcy56kdK0M/wB4ekiIAD5gKjuLMT7XEpXb2BrxNfiXrr/wp&#43;VbPhz4i3t7qAtL5VUPwGHFAHpF0YZF8qRhn3rMvrO0t7KaRI9zKua57VNQd9QgXecZwSKvXPiawjt2t3lTzCuOazNDy3WL6S5v3BULGDxjvU9h0FZV82NUcDoz5FW7aSSO68tP4qAL17jdxVJP9YtXrmDywu8/O1UDx/31QB6h4Hmzpjp711Ga8r0XxLBo1q6Tsy7vu4Faf/CwbHH&#43;tf8AKgAs9Ln1qzSa2AVBx81X7PwlcW0RLuN&#43;e1bPhKHy9Bt&#43;Oq5rdl/1RoHUOUsL6S3uHs5Wyy/drXF1GRln2v6GuTv8jUJmVmDZ4NYs17qaMcSsa5joseh/a0zy4pupJZatp72kkijcOK85&#43;2apJ/y0NAuNVMgxKfzrUfszWs/AKxTk7TKAavP4FRn&#43;0RRGOZOVwa2fBiXbW0s91MztnG2uo71octU84l0jUVfdLFKXHQqeKwtR8OarcTfaPsxG3nOa9ekGQwxWTqsnk6bO3otaB7Q8H1Ast6CwwQ2DV1JvLnjNU9VOb1m9WzTpjiCOT0rMDYvJvNmh5qvjlh/tVDDJ5rwvVof6&#43;XjtQBHfWc1xaoIozIc/wiso6bdL1t5B/wABr0TwSVGo7XUMGHQiu51W2tYrCWRok&#43;76UAWdNtha2cUH9xQKtS9DXMi&#43;uT0Y0v2q&#43;P8AeP4Vn9YMzJvxm/m&#43;tZrREk5xWhMkpmdzG28n0qslrPIxzG9ZHoor&#43;V9KEiO/5cVf&#43;xTY/wBW35VH9iuR/wAsm/Kgdzq/CgP2B/rW8eOvFcbpU9/ZJ8qNt/u4q3eXup3KfICv4Vr7Q4av8Q17jWLaCXyEKs7dazdWbzoFhH8ZNYX9lXpkaUht7d6ntZLxLwxSRmTy1/nWntTM8q1&#43;LytSeMf3qPL32X0q54ij/wCJrISMNuNQwRF7J3oNCtp8nzKP7rVsScT5x95ayNNGZ3HvW/JF&#43;8i&#43;lAG34QkxrMQrvPEG/wDsmbb/AHa8/wBB3218kyJudT0rsbvUZb22aB7d13UAbHlxR/ctV/Kn&#43;c5&#43;7Eq/hXnsWsX&#43;f&#43;Ph/wA6vRaxecZmagDs8g9VGfpTMoP&#43;Wf6VzP8Abd1H/wAtP0p/9v3WOooA6TeP7n6Ucf3B&#43;Vc5H4jufRalj8RzfxotAXN7A9KMD0rH/t1/&#43;eQoj8Q&#43;sA/OgDQuZJAn7sHNYm&#43;&#43;jmllFv8AM3G6rcniQdPs/wCtH/CSQIMSW&#43;2gDidS8K3mo3LzkdecVUk8O6glv5ENvt9zXoI1u3k6REVZt72zl/1gNAHltr4dvLBWMkO5m9K0p9PljELEYr0A3mlmXYWFSvY6ReD5pBQBx&#43;l2/kXEbr8zCu5hkiKgkDOKybuLSNOmhHnHc3QDmteGO0kiEiSjbQB//9k="
     data-sizes="auto"
     width="2325" alt="A statue of James Brown singing from downtown Augusta, GA"
     loading="lazy" />



  </a><figcaption>Okay James Brown wasn&rsquo;t known for speaking, but you get the point</figcaption></figure>

<p>Public speaking is hard for a lot of us. Having some suggestions about how to do it well can be part of helping us learn to power through.</p>
<p><strong>The Art of Public Speaking</strong></p>
<p>~ Dale Carnegie: The original, and classic, guide to good public speaking technique.</p>
<ul>
<li><a href="https://search.worldcat.org/title/818844916">World Cat</a></li>
<li><a href="https://www.amazon.com/Art-Public-Speaking-Original-Improving-ebook/dp/B079Y99V7W/">Amazon</a></li>
<li><a href="https://www.barnesandnoble.com/w/the-art-of-public-speaking-dale-carnegie/1126664230">Barns and Nobles</a></li>
</ul>
<p><strong>Speaking Memorably</strong></p>
<p>~ Bill McGowan &amp; Juliana Silva: A 2025 guide on good public speaking and modern presenting.</p>
<ul>
<li><a href="https://search.worldcat.org/title/1455748854">World Cat</a></li>
<li><a href="https://www.amazon.com/Speak-Memorably-Art-Captivating-Audience-ebook/dp/B0DGLJ62J7/">Amazon</a></li>
<li><a href="https://www.barnesandnoble.com/w/speak-memorably-bill-mcgowan/1146276746">Barns and Nobles</a></li>
</ul>
<h2 id="other-material-i-think-is-worth-your-time">Other Material I Think is Worth Your Time</h2>
<p>In addition to the published works above, I think these resources can be useful to a lot of people.</p>
<ul>
<li><a href="/tags/writing/">Posts about writing for developers and technical consultants</a></li>
<li><a href="/2019/11/some-things-every-developer-should-read/">Some classic works</a> in our field
<ul>
<li><a href="https://web.eecs.umich.edu/~weimerw/2018-481/readings/mythical-man-month.pdf">The Mythical Man-Month</a></li>
<li><a href="https://www.tracykidder.com/the-soul-of-a-new-machine.html">Soul of a New Machine</a></li>
<li>Just about <a href="https://stackoverflow.com/q/1711/24215">anything on this list</a></li>
</ul>
</li>
<li><a href="https://stackoverflow.com/q/1644/24215">Listen to good podcasts</a>
<ul>
<li><a href="https://www.hiddenbrain.org/">Hidden Brain</a></li>
<li><a href="https://freakonomics.com/series/people-i-mostly-admire/">People I Mostly Admire</a></li>
<li><a href="https://freakonomics.com/series/freakonomics-radio/">Freakonomics Radio</a></li>
</ul>
</li>
<li>Trailhead Modules
<ul>
<li><a href="https://trailhead.salesforce.com/content/learn/modules/soft-skills-for-sales">Soft Skills for Sales</a></li>
<li><a href="https://trailhead.salesforce.com/content/learn/modules/manage_the_sfdc_storytelling">Storytelling &amp; Communication </a></li>
<li><a href="https://trailhead.salesforce.com/content/learn/modules/writing_style">Writing Style</a></li>
<li><a href="https://trailhead.salesforce.com/content/learn/modules/virtual-presentations-public">Virtual Presentations</a></li>
</ul>
</li>
</ul>
<h2 id="practice-suggestions">Practice Suggestions</h2>
<p>What you do for practice doesn&rsquo;t matter a great deal. What&rsquo;s really important is that you do it and get feedback. Here are some things you might consider for practice:</p>
<ul>
<li>Short Stories or other fiction writing that you publish online, ebook, or print on demand</li>
<li>Contribute project documentation to an open source project</li>
<li>Essays for Medium, or other blogging platforms</li>
<li>Participate in online writing challenges</li>
<li>Create a YouTube (or similar) channel</li>
</ul>
<p>Whatever you pick, you want to be in control of how often you create content, and then make sure you create content on a regular basis. If you aren&rsquo;t in control, you might find yourself cut off from chances to practice.</p>
<p>I strongly suggest you don&rsquo;t use AI during your practice. You need to do the work, even when it&rsquo;s hard.</p>
<h2 id="closing-suggestions">Closing Suggestions</h2>
<p>Generally, I recommend being <a href="https://bigthink.com/the-learning-curve/5-habits-increase-curiosity/">curious person</a>. Push yourself to <a href="/2022/08/my-ongoing-liberal-arts-education/">learn more</a>. The more you spend time finding good information sources to learn from the more examples of good communications you&rsquo;ll have for inspiration on how to get better.</p>
]]></content:encoded> </item> <item>
      <title>Prevent Salesforce Custom Metadata Text Field Truncation</title>
      <link>https://spinningcode.org/2025/prevent-sf-metadata-truncation/</link>
      <pubDate>
        Sat, 04 Oct 2025 04:00:00 +0000
      </pubDate> <guid isPermaLink="false">https://spinningcode.org/2025/prevent-sf-metadata-truncation/</guid>  <description>Learn how to prevent Salesforce from truncating Custom Metadata text fields and discover the simple solution.</description> <content:encoded><![CDATA[<p>While working on my <a href="/2025/salesforce-email-invalidator-tool/">Email Invalidation Tool</a>, I ran into Salesforce truncating Custom Metadata text fields at 512 characters. I struggled at first to understand why the static methods – my goto with Custom Metadata – were resulting in broken records. Turns out the solution is simple – use SOQL.</p>
<h2 id="the-problem-salesforce-truncates-custom-metadata-text-fields">The Problem: Salesforce Truncates Custom Metadata Text Fields</h2>
<p>When loading Salesforce Custom Metadata records using the <a href="https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_methods_system_custom_metadata_types.htm">static methods getInstance() or getAll()</a> long strings are truncated at 255 characters (I actually ran into it at 511, but the documentation says 255).</p>
<h3 id="my-use-case-and-reproduction">My Use Case, and Reproduction</h3>
<p>One of the features I wanted to add to my email invalidation tool was auto-detection of email fields. That&rsquo;s turned out to be a whole complicated process that <a href="https://github.com/acrosman/SalesforceInvalidateEmail/pull/8">I&rsquo;m still finishing as of this post date</a>. Part of the solution I am using is to create a custom metadata object to store a list of possible fields. For a variety of unimportant reasons I elected to store that information as a JSON string. So I created a long textarea field maxed out at 131,072 characters. Everything went fine at first: the process scans the objects and drops the results into the metadata record. When I looked at the records in the UI or after downloading to a file it looked just as I expected. But when I started to load the records again I got JSON parser errors because the value had been truncated to 511 characters (see note about about 255 vs 511).</p>
<h2 id="preventing-salesforce-custom-metadata-text-truncation">Preventing Salesforce Custom Metadata Text Truncation</h2>
<p>The answer is to use SOQL. I typically use the static methods when working with Custom Metadata to reduce the number of SOQL queries in my code – but in this case that doesn&rsquo;t work. To reliably get all the data in long text fields you have to use SOQL.</p>
<p>This approach consistently gives you a truncated values:</p>
<pre><code>Email_Invalidator_Cache__mdt cacheRecord = Email_Invalidator_Cache__mdt.getInstance(
  'Field_Scan'
);
</code></pre>
<p>But using a simple SOQL statement to load the same record works just fine:</p>
<pre><code>Email_Invalidator_Cache__mdt cacheRecord = [
  SELECT QualifiedApiName, Cached_Value__c, SystemModstamp
  FROM Email_Invalidator_Cache__mdt
  WHERE DeveloperName = 'Field_Scan'
];
</code></pre>
<h3 id="ask-salesforce-to-fix-this">Ask Salesforce to Fix This</h3>
<p>There is <a href="https://ideas.salesforce.com/s/idea/a0B8W00000GdakyUAB/custom-metadatas-getinstance-doesnt-retrieve-complete-long-text-area-fields">an idea open requesting a fix</a> to this issue. Please up vote.</p>
<h2 id="special-thanks">Special Thanks</h2>
<p>Thank you to <a href="https://www.robbieduncan.com/">Robbie Duncan</a> for helping me out on <a href="https://www.linkedin.com/company/ohanaslack/">Ohana Slack</a> with this one.</p>
]]></content:encoded> </item> <item>
      <title>Salesforce Email Invalidator Tool</title>
      <link>https://spinningcode.org/2025/salesforce-email-invalidator-tool/</link>
      <pubDate>
        Mon, 01 Sep 2025 00:00:00 +0000
      </pubDate> <guid isPermaLink="false">https://spinningcode.org/2025/salesforce-email-invalidator-tool/</guid>  <description>A Salesforce tool to invalidate email addresses in an org to prevent accidental messages.</description> <content:encoded><![CDATA[<p>One of the nightmares of most Salesforce Administrators is the day that their full copy sandbox sends fake email messages to real people. To help us all sleep better, I created a tool to <a href="https://github.com/acrosman/SalesforceInvalidateEmail">make it easy to invalidate all the email addresses</a> in all the email fields in your sandbox.</p>






<figure class="figure">
  
  <a href="/images/posts/sf-email-invalidator/InvalidatorScreenShot.png" target="_blank" rel="noopener noreferrer">
    
    

    











<noscript>
  <img class="rcf-image" src="/images/posts/sf-email-invalidator/InvalidatorScreenShot.png" alt="This tool has it&#39;s own app called &#39;Invalidate Email Addresses&#39; show in the picture" loading="lazy" />
</noscript>

<img class="rcf-image lazyload show-if-js"
     data-srcset="/images/posts/sf-email-invalidator/InvalidatorScreenShot_hu_a91cdb7bc64761ee.png 680w, /images/posts/sf-email-invalidator/InvalidatorScreenShot_hu_303ad7ee38729dc9.png 850w, /images/posts/sf-email-invalidator/InvalidatorScreenShot_hu_ea242e6eb916dfe5.png 1020w, /images/posts/sf-email-invalidator/InvalidatorScreenShot_hu_c9eccb12f90e2603.png 1360w, /images/posts/sf-email-invalidator/InvalidatorScreenShot_hu_96f48e56ecf69c66.png 2040w, /images/posts/sf-email-invalidator/InvalidatorScreenShot.png 2994w"
     data-src="/images/posts/sf-email-invalidator/InvalidatorScreenShot.png"
     src="data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAAA5CAIAAABxmbQhAAAPt0lEQVR4nOw8a3McuXHdQAMzs&#43;RJOuris&#43;&#43;ccpWTD6nK//8ZqUo5&#43;eYPcfniR51z1okiuTsDoDsF9AwWO7uiKIniI5WmjkfOYnqAfr&#43;G9ONV&#43;N1ft//2p&#43;vLMb3e0L980//rt8O3584TInxxEBFmjjGmlJhZCgAAIpoC&#43;vPnPwUAjDFEZIy5F4QppRBCjBERrbUAEEIAACLSzVtr9bl6xv1hE//tp8v/&#43;uNfxin&#43;4puXNCW5CXw18Zsts8RvNukmZEqAIDwABw5PVfeqZFIe3Au9lBb3CCISC&#43;j2Qgjv3r0DgGEYbAHvfSthAMDMKSUBk5jJWiYJMZE1MDj8erAI8LK3Xw&#43;2JzR3P3JmVTne3U6Yt1v/NRcVlPQigg18GoHWD71vaOWj3SoeQlXo/R7QnA3dxdcvEnPvHZ1785uX3gpvA7/s7bdf&#43;VedQeGU&#43;JYD1AdKSjJNMo2cEqicHWqO6H96EdEQma5D56GoraKpRG9NkD70HiX3fjlqjPHeq8FBRCJSQ1RNUNVdESGi5izofTf0HQsQGQwx3Yzh7c0YYuwd9c4YYeHZImdc5QsKHbNhKkiJqOs6awyEEK/ehbdvw26XYszPyLeU9XpTMezl9GCJ/ObMv3xlz87AOVT2LCbiJK0R7s0SYtkZrCXkU0B04zL/gIACe9GZH9dwenW0Yi9YxZLiFDAGn0aTooE4BQnTNI5TCEGEszPJXxYRON8iegjf&#43;fPz8853mGIYw2437q5vQgjMrPyvO&#43;ACKibO&#43;41xMgRyARn2dJgP9GUBAR7Yq90CC0eE/vzDj8xxmqaUki2ECyFMxb8L86xLRZvU0GNZY629GrbeeyOSdrtwM4axBDIimEnPVQ6kqEDhB9gYb3jrozGXu4wH94pVd7YXT/2/3Eo1aVYuV05jay7dA7Y7MlJuw6ZA//6ff&#43;i9JczSrZKrjpurXy1&#43;s3204rDmKmsGAKQkKXNLfXF75rp4EcCINpo3W8wqhdVDaLBWRfSklT55ZDn89ADbgTn4MKq7YGvV6IPiIUePk8OT6uboD3/fff9683IgiwIGjSUwxi53NPza27S9uZ63U6S&#43;4D2WuD0JZns5xz&#43;yoJuRLQjLQmwPuTrPiq/Fv7S/7rEt16tRPoHqg9j25gLrSjxYc4L4&#43;48OkR9ws4gqknTd&#43;YvhohMTR0PkzwfbD1DCkkp/3DtVVDxz3LmEFUqw47D9WJZF9lK0CNGRXp6S29s14FGw3a4EH8ZWqEZ9586sdOOlXF1a77sNWduJkaTek7OkZj88a8Vs0OtHJeTJNC9hWQ6NiNwq1MOjDX0yvE8nngK2TwPqPZEBEyKngMlASpxCjBLVpZZoiYgcUfatOZEDgzgndUVhVUFy8mcQu46yeTePdJznB&#43;QdWe/IvRBrrXO0ObNdTotdCTprFUWzjBKJgkZEpWawlxtjDDkHiCmv4Va6Pj/2O1bn/cWj1O/uqD5/b5&#43;PjSzZaH3wznTnYM1Eg4VMRzB77IxQ7E3xtvVR9sCiJQROGLYJkI8fg8cx3oqQ&#43;J6fTxmHfQa3eJXZtZ&#43;IYz4C24m9tTb&#43;CNtJVO&#43;DkyYu&#43;wBr7CT2Jtv5zhg0yeDIh25U4Mhznj5khnTiaU38dDrGvH3bJ7EV4xgTp8TWGK1ywj7IONrbexhw&#43;hB3ytlqMgy3e/piLSBxsebNIQyitUBngx&#43;6zhonDMZC56QYG5WrkmTXcKj8quYIzW31uvtV8/dEFNlb3ezCliORPRs8WaM2k2t98LCUIYffalxdY8OammS/phWYD4nLB08qACHKu218O6bdxHVXzuJXvX3VE531fuP7GFyYhDCRDxaCSBKR2e6L&#43;tscnVtLHXWbzdB1XrO2xwIBiJEvr0e6GTede3neOWc5pamANhiyZ5qDNKwVp1ryq/0GFuY018AVuRbaSkRHn1m8Y5Z3u/Q/N7sf3sS/X0VezPPgzW/&#43;wfzioqPO5QhHoi2JGktKMe1SCnnHlkrnoQRE5VZ25CyQ3fSebJa4T97c7RWGu9wvkTjEFGLqOxp658jEAHHiGHa77S6EAFgyS2s0hmZOSncuEYQxJod2gCnFEGI&#43;riXNsBw5Z9HZvuvIoPmcUyaWm4A3E/z55/jj28iLGdp05quNZ7BEFjuPHWHfYedd7wHQcopVImqjSnWi67qy77lzonBQ&#43;WtKTe2nx8XO9vodBa1drAGwaf8Z4xz1XYcAzhEcdtZqRFfrg0toR0SRmW3hldZ6nXf5txPV649JHnJ6hGRx09mLM5dYahli8ObFhjpnCBCI2NsoXbb&#43;JYp3zFbpW3dZ9dcYM02Ttt/ai7Wsf1R6lRWh2zZFy5jVsvb6qrBa/BChsfui4hIWO&#43;eMMcMwHN/eYq6ng6YT1zZYlBlqx&#43;CglyAraYBTHbf9QwF6h//42nsL12OvDMgkJnz9lXt1RlTuTsJT1lCWGKSU15LafWut2sHaHqg2VJaNY&#43;m11o5EqxnthmqvUfm0On/thbVkqj8rZrXseffOeS9aQYTaHQxJO391z61iNfvl2n8&#43;FoK2mqK&#43;RFmuB2wLLbXfq/1hXVnPW3Iqp9s2AK8G2BAlaXpQiN7Z3htawgIGSXJKHnUHVZDr1vU0&#43;TrnsNWVR&#43;4PU6VFebZQoO0TNeW90y2wVXfMOVdNR9EAg5CgNDdiDNO4G8cREfu&#43;997bUnCt2Oa6ikgIYRzHSq9WI51zfd/XU4QQdrvsS4wxfYGVT25x7na7WPpRei7v/TAM3vsiVYDCzrCbH1diUIOl7AmEhe3O5fBzZe8qyap0qHLMj48xjSNPEwibGA3n701oV&#43;pE3tvOs8WJQyo&#43;kIFXDLBonSGyzqI9Vp33ASKmJIBRn6aRp5oRpQvc2oOsMnS7&#43;2n15vbO/grJXNJnXhQRDou/KKX8DyIERb&#43;GwVmzRtc&#43;b2V8gDlcXd38/Gb8y1/j9mZEHMkZexCYGnLdN6&#43;HX38/nbu/jT9djpdR4mrfBs3ghov&#43;9UV/QbaEHHeDcorUbs4Yq61XZp6mSV1FNR1qFmolUb&#43;34k9EvkDrsdWmraY0WipbOztLtdjtTE3VhlnOytPVXjWMQdI6WudLTe7OwCFwmKY//vD2P343vXmTt1jsHTSSZb0//&#43;1vv/b2HQ6/f/f7P13&#43;9y6MSztiBrJ0cfb6n77&#43;54H6znZ3D/hwlZXnM6OKfw2aTTFVyoZKMrUYyywAzs6pSGEO8JxrhY&#43;I5hxoKYgdZz&#43;taW0pXsMT9QRKGw2FsxVZlpFmiku&#43;ctdYMK9kkWni7ZZ3OzBG6r7nVppwDGm7zd8T7eL2arza7rZSTlvrL9nsun5KUw4A4COmkWrbRdTeoSHnEY32RGeuFKVAY9q2jDHW&#43;Q6NFeHSTM&#43;4siTm8Mlm43Cw0mORZWvJWAI0cmp7xmacxlipXSnEkn7oLcvTUX2dKd3Y/GhmIVisZwK5e9aXed73/fffvZjGdH3dWrgaGiLR5le/8i9ebbz/7ux7B36appqLKwOstS&#43;GFxf&#43;wqETzl933MDclBJIicdJrEFrEIAEDypyU8SQA6fU7lwYuUieNpYKuTAmTMLYVBLLSsPgACEJjBNPJbk7SQ1mI&#43;gKQ2tshinIlP1iuzInUFnOEpsQERPllDKl7XYLcuC4jr3KKkDM/u6Xvzw7P5cYsdSk2jUqgHS2gc15h/a7/tcX9nUs6pxJV0s1CM76MziDALu0yxxoJuOOia4WQ&#43;NCKPI&#43;hnS1TVfbaD9imuzgYHCydfcgUERGsqu5uUkpTtBMSlUe1MylepV9DEoOX75qSy0wWxhUO5WsZRGY2KMnpCxksFRjoDQcWJDzryOPIQtr0mBOrW27h8oVZUDXdZZctoKRL6&#43;DyHSPE0QPBIX1BLV6IPvcWvOd6tDbmLeNHEBHS4o5E438Fkwzq6yV2ayiEbWGxcoXT8YipcUmwMBJwHBls2aCOmFJ5JQXNT7JqUAOyUtYzDIF7dQ9S6C52o9GM5dMzZkKdSClTFkZQSVVCcb2XQqpnnBuEKtyFOaVCk0pXGs/Oju92TqVZJBZzXAmpfY2jTV1xqwMgVkdwVv21jZPEuvMkcwl6OcJlFhiklRK/FqnWCILHTjRqKCwwYrRjooyBmo/Qu2&#43;VOODmH2htuphxiCQIwjB2tvJnwpmAzQ3tFh7btYWVs0aFtmMgU06MtMCiXk3FbfyOKS7H6CYeDumnJY1MybS9oTmDtQ&#43;PmsogUcXcN8lklUPE/dVUlxu1AX7lXgwpygYMpXDyUZHYrnehhD50SYa7gNIOxJjCCBMlnyJpg8oLbKMoFZmtF2lZXz3SznA9&#43;EtydH9DDs/JpA1YDBO4TqEse96MpucpLAsxR9ZvK8Yg9qy0CHQOpbdu5xClnzvQYkR8y7g7c30rHlA2cUih7DdjTcIySFYMNqia4vALGKN8Z03aKYwcZoLKd77vrPebbxbz2N9USgtybQdyT5qZ/TzgQyiI4K&#43;t0aGbth0vUUtWmFpOu4D0PqOVYxdrX3nSNE5WqL2B9u3CJjy3sKzFn9lgNn0fXfuUwoGjSOnJck2Hat5aY3Ea3dbK1Z2mXZ&#43;MDg99/wMgbIZIXe&#43;GUrVLHFTRK1WXvt8J6vesHoB6qHhOQdABebONRkrLgt4qm8mLc25tiSwglV/7v/hE2AulAOCLelpbUq1SlC72E8K5KB68lyB5hqp7HOm2l5oewtPUMbnV3KeOdDcUi2TEXWgTPtzjzv7djvMBSCR564CczW0jBZwKLAay3my8NT3dzcgWIZwdbyn7SDXovTdJ9ceEpZXo57cxj4KaCkmi9K9hpvtVMXyruqBRXp0lhh5/pUgABIQTmkcOaXIKR2vqKPCbTB6y8jNg4GRUsB&#43;5iwgEIgp7XZx3G3Texiw2WxUOeqARv30sXjwf8MBqAbA0TgzrGaz9vNY8&#43;KPmJ/4UlDftHjmrJgHs3pP3tn3zdXWPwtSLz6FGGnpUzxvICivAfe9N9i1H9RJiJMjeVUJHkYVTmckMg&#43;FPsAGvhyQttLLdHnr0UQnLKcp6MhfTAlvfdWirQut8uf1e9vNrHWFk28JaP9Yx81XBdflDxPhc5tFWQOJzizmvHJ/Pu24jlPY7nbzeL7o&#43;x77wVXId61nuY5nh3RCS/&#43;iTvvSHDY15T2ryuJqXrQ&#43;Qs51y&#43;sw0NRLynuHz18DQuSr7VRemTzWAA6x0DXV0QeuY4VSO&#43;/zrAnMA1cVhcjyFrUs83pVrGVu/jcr5/cykVdKZlMa42RNOnx7VFKSq&#43;0U4vMdSclA2zH&#43;9PP23fW0aqvrH3VajXIukzlSF0EdPNWhLFhGr9q/TYFw/Do0HjxrH800eGZuIYSTOQeLTCFtx&#43;fNAQqBL1MwGG/7mx9P08yq5eSnEJF9OhALSJIEJ&#43;eunza0ivNs4X8DAAD///jvL/tyNM8MAAAAAElFTkSuQmCC"
     data-sizes="auto"
     width="2994" alt="This tool has it&#39;s own app called &#39;Invalidate Email Addresses&#39; show in the picture"
     loading="lazy" />



  </a></figure>

<p>When I <a href="/tags/data-migrations/">migrate data</a> I often add <code>.invalid</code> to all the email fields in the org. More than once this saved my team from accidentally sending a large number of messages. Having a tool to do this easily in a sandbox has been on my todo list for a long time.</p>
<h2 id="why-invalidate-salesforce-emails">Why Invalidate Salesforce Emails</h2>
<p>Salesforce automatically adds <code>.invalid</code> onto the end of email addresses of Users to avoid accidental sends from sandboxes. This is great when you have an experience cloud portal setup. Because Salesforce&rsquo;s core platform is not an email marketing platform Contacts, Leads, and other objects with email fields are left unaltered. But lots of organizations <em>use</em> the core platform to drive a marketing platform. The use add-ons and data syncs, or they use the org to send transactional email directly from Salesforce. When the org is the tool that initiates the send, there is a risk that a sandbox with real data can send invalid messages. I&rsquo;ve seen sandboxes send <em>a lot</em> of messages during testing of new features or when an automation was left switched on that should have been disabled.</p>
<p>Most Salesforce consultants I know have stories about times when an org sent unwanted email messages to a client&rsquo;s contacts. Those events are embarrassing at best. At worst they can expose you to financial or legal liability.</p>
<p>Often these events happen when someone makes a simple error with a full copy sandbox, or an org with migrated data under review. The story outline generally is that while testing an automation, or a scheduled process, a team member sets Salesforce to send messages. They forget to disable email again, or they don&rsquo;t think about a scheduled automation set to run at 2 AM. Minutes or hours later that sandbox triggers dozens, hundreds, even thousands of emails.</p>
<h3 id="cant-i-just-disable-email-in-salesforce">Can&rsquo;t I Just Disable Email in Salesforce?</h3>
<p>You can, and should, <a href="https://help.salesforce.com/s/articleView?id=platform.data_sandbox_email_deliverability.htm&amp;type=5">disable email deliverability</a> in most sandboxes most of the time. But in my experience that&rsquo;s not sufficient all of the time.</p>
<p>That setting is good, but less than perfect. Sometimes you need deliverability switched on to test features or deploy changes. Sometimes you use a 3rd party email system that ignores that setting. By actually breaking every email address nothing can slip through.</p>
<h2 id="how-my-salesforce-email-invalidator-works">How My Salesforce Email Invalidator Works</h2>
<p>This tool uses a series of custom metadata records to determine which objects and fields have emails you want to invalidate (it comes with a sample record to invalidate the Email field on Contact). When the batch process runs – either from the provided app or post sandbox copy – it will add <code>.invalid</code> onto the end of any existing data in the field. It does not check to see if the value is actually an email, but it does ensure <code>.invalid</code> is only added once and ignores blank values.</p>
<p>The tool provides <a href="https://help.salesforce.com/s/articleView?id=platform.apps_overview_lightning.htm&amp;type=5">an app</a> that gives you a place to see and update the exiting field list, a button to initiate the process, and allows you to replace the <code>.invalid</code> text with a different string. You can also have it automatically run after a sandbox completes its process by entering <code>InvalidateEmailSandboxPostCopy</code> as the Apex class to run when creating the sandbox. I created the tool to be metadata driven so that the setup carries down from the production org into new sandboxes.</p>
<h2 id="security">Security</h2>
<p>Please be careful when installing and using my email invalidator. It is designed to break data – that&rsquo;s the whole point. I have not put it through Salesforce security review, and I don&rsquo;t plan to bother. I&rsquo;m not even sure they would approve a tool designed to break data. It does have a permission set that should be giving out on a <em>very</em> restricted basis to highly trusted users – ideally don&rsquo;t assign it to anyone in production including yourself.</p>
<p>I am releasing the invalidator tool <a href="https://github.com/acrosman/SalesforceInvalidateEmail">as an open source project</a>. You are free to review the code and validate that it does what I&rsquo;ve described. I will do my best to answer any questions you post to the <a href="https://github.com/acrosman/SalesforceInvalidateEmail/issues">issue queue</a>.</p>
<h2 id="installation">Installation</h2>
<p>The current setup is just a collection of loose metadata in a repo. Until I get around to packaging it you&rsquo;ll want someone who is comfortable with git and sf cli to support installation. You should clone <a href="https://github.com/acrosman/SalesforceInvalidateEmail">the repo</a> to your local machine and push all the metadata into your org with sf cli&rsquo;s <code>sf project deploy start</code> command.</p>
<p>Of course please install in a sandbox first and test it out there before putting a copy in production. To make it easy to use over time it will need to be in production.</p>
<h2 id="feedback-and-suggestions">Feedback and Suggestions</h2>
<p>I am open to, and interested in, feedback. Currently the tool is a very early alpha quality version. It works, but hasn&rsquo;t been tested in a lot of use cases. In the <a href="https://github.com/acrosman/SalesforceInvalidateEmail/issues">project issue queue</a> you can offer feedback, report bugs, or make suggestions. I&rsquo;d also enjoy getting code contributions as well.</p>
]]></content:encoded> </item> <item>
      <title>Thoughts on Professional Development</title>
      <link>https://spinningcode.org/2025/thoughts-on-professional-development/</link>
      <pubDate>
        Sat, 05 Jul 2025 01:00:00 +0000
      </pubDate> <guid isPermaLink="false">https://spinningcode.org/2025/thoughts-on-professional-development/</guid>  <description>We all have to constantly be learning. These suggestions are more or less in the reverse direction of importance. Keep reading.</description> <content:encoded><![CDATA[<p>Ongoing professional development is a fact of life for people who want a long-term career in technology. That means finding ways to learn new skills above and beyond what you&rsquo;re paid to do. We all need to strike a balance between having a life outside of work, and making sure we&rsquo;re learning new work-related skills. Your professional development should not take over your personal life, but if you only learn on the job you will limit your options over time and be bound to the generosity of your employer.</p>
<p>I changed jobs recently. In the process of leaving my old job I was asked a lot of questions about how I&rsquo;ve built up a wide-array of skills and knowledge. During those discussions I tried to give the best advice I could on the fly to a question I was asked cold. Now I&rsquo;ll try to assemble a set of more carefully considered advice.</p>
<p>These suggestions are more or less in the reverse direction of importance: the more you read the more valuable I think the suggestion is.</p>
<h2 id="directed-learning-job-related-learning">Directed Learning (Job Related Learning)</h2>
<p>By &ldquo;Directed Learning&rdquo; I mean learning that is supported or encouraged by your employer. This can be certification preparation, employer provided trainings, or research into an emerging technology, even building tools that directly support your work. Into this category goes anything that can lead to higher pay, advancement, or even just keeping your current job.</p>
<p>Some employers are really good about this. Over time I&rsquo;ve been sent to trainings, had course fees covered, and other direct support. The job I just left even provided financial incentives (cash) for us to acquire select certifications of value to the company. Other employers have published a list of what they wanted people to have – without direct incentive. But most often I&rsquo;ve had to figure this out on my own which is not ideal of anyone involved.</p>
<p>This type of growth has a few benefits. First, it can help with your reputation (and hopefully pay) with your employer: &ldquo;Oh, Aaron is certified on <code>[interesting product]</code> we can assign him to <code>[high profile project]</code>.&rdquo; Second, it helps make sure you are growing in alignment with your employer&rsquo;s needs. That can be helpful when they are forced to make cuts: &ldquo;We should keep Aaron, he&rsquo;s the only team member with <code>[rare certification]</code> and Salesforce wants us to have that on the team.&rdquo;</p>
<p>In my life as a Drupal developer this looked like creating modules for internal use, or <a href="/2017/01/drupal-8-plugins-are-addictive/">learning new techniques</a>. But it boils down to the same thing: learning stuff your employer needs you to know.</p>
<p>The downside of only learning what your employer needs is that you miss chances to grow beyond their needs. That limits your future mobility – your best chances will be for another job like you have today. If you want a <em>better</em> job or a job doing something different, you need skills your employers doesn&rsquo;t think they care about.</p>
<h2 id="personal-learning-side-projects-and-other-self-directed-research">Personal Learning: Side Projects and Other Self-Directed Research</h2>
<p>To learn things your employer does not care about you will likely need to put in your own time. You should focus this kind of learning on something that interests you – make it fun. My experience is that this kind of learning goes best with side projects.</p>
<p>Side projects allow us chances to test and practice the ideas we&rsquo;re learning. I use them to learn programming languages, frameworks, and techniques that interest me but not directly useful to my work. <a href="https://github.com/acrosman/Salesforce2Sql">Sometimes they come around to being useful in my work</a>. Sometimes they just give me a chance to learn interesting things. <a href="https://github.com/acrosman">My Github account</a> is full of examples.</p>
<p>An early professional mentor told me to <a href="/2022/10/thinking-about-languages/">learn a new programming language every year</a>, and so over time I taught myself a bunch of programming languages. These days I generalize that advice a bit and try to learn a new platform, framework, or technique every year – and sometimes a new language. That meant when Salesforce rolled out LWCs to replace Aura Components, I was ready to learn them quickly. It also means when a friend started to talk about using <a href="https://wails.io/">Wails</a> I could related from my experiences using <a href="https://www.electronjs.org/">Electron</a>. My experience creating <a href="/2022/01/project-estimates-tool-2-0/">a project estimation tool</a> helps me discuss estimation techniques with project teams.</p>
<p>This blog is an example of a side project. Every post gives me practice at writing. To keep posts accurate I include research as part of my writing process. That research teaches me things I wouldn&rsquo;t learn otherwise. The act of writing forces me to clarify my thinking on topics. All of that is useful to me and helps me be better at my work. I also know this blog as been read by colleagues, employers, and potential employers all of whom are useful audiences when getting new work.</p>
<p>When I give talks about <a href="/tags/writing/">communication skills</a> I tell people what they do for practice doesn&rsquo;t matter: <em>what matters is that they practice</em>. Same is true for side projects. What project you tackle isn&rsquo;t important. What is important is that you do something that forces you to learn new things.</p>
<p>Side projects help us <a href="/2016/08/different-angles/">look at our work from different angles</a>, and therefore test our limits and grow.</p>
<h2 id="develop-learning-routines">Develop Learning Routines</h2>
<p>It&rsquo;s easy to get lazy or to put your professional development aside – it feels like there will be time later. So you should create a routine that works in your life.</p>
<p>I like to have a routine to make sure I&rsquo;m steadily doing some form of professional development – it&rsquo;s a marathon not a sprint. For me that generally looks like job-related learning for about an hour after my formal work day and working on a side project on weekends. Attaching your learning to your work day allows you to more easily block the time in your mind and with family.</p>
<p>I know people who like to do their study before work because that&rsquo;s when they are fresh and study gets their brain into the right gear for the day. Other friends tell me they like to do research or study after their kids go to bed because it gives them more time with the most important people in their lives. All of those are great patterns because they are tailored to their life.</p>
<p>Whatever routine works for your life is a good one. The important part is to making learning a regular activity.</p>
<h2 id="share-what-you-know">Share What You Know</h2>
<p>One of the best ways I know to learn is to teach. Giving talks, writing blog posts, and mentoring all give me a chance to share what I know with other people.</p>
<p>Sharing can be intimidating. I have been writing this blog for nearly 10 years. I gave me first conference talk more than 10 years before that. And still, every time I take on a new topic I&rsquo;m nervous I&rsquo;ll <a href="https://health.clevelandclinic.org/a-psychologist-explains-how-to-deal-with-imposter-syndrome">be found to be a fraud</a>.</p>
<p>I try to leverage the fear I feel to help me assemble the best content I am able (at least within my time and resource constraints). When I am writing, or preparing a talk, I also do a lot of fact checking. Since I never want to put anything into the world that I can&rsquo;t back up later, I do my best to make sure I&rsquo;m right. In the process I also learn more about the topic. That even means correcting information I was preparing because my initial understanding was wrong.</p>
<p>On the flip side I try not to let the <a href="https://en.wikipedia.org/wiki/Perfect_is_the_enemy_of_good">perfect be the enemy of the good</a>. I require myself to post on my blog once a month – limiting my editing time. Similarly conference talks have a hard deadline. Those deadlines force me to complete content on a schedule, not when it&rsquo;s perfect. So be it.</p>
<p>When you share what you know people start to see you as an expert. That can lead them to ask you questions that push you to learn <em>more</em>.</p>
<p>All that can lead you to <em>be</em> an expert.</p>
<h2 id="make-friends">Make Friends</h2>
<p>Making friends is easier for some of us than others. And maintaining <a href="https://www.gse.harvard.edu/ideas/usable-knowledge/24/10/what-causing-our-epidemic-loneliness-and-how-can-we-fix-it">friendships in our current society is extremely hard</a>. But friendships are super important. They help keep us healthy, mentally fit, and happy. Friends also teach us really important things about the world around us.</p>
<p>If you have read this blog much you are used to seeing me make references to my friends as sources of information. They are my greatest resource for learning (and a bunch of other things too). Friends know stuff we don&rsquo;t know. Even better, they will <em>tell</em> you things you don&rsquo;t know. Good friends don&rsquo;t judge you for your gaps, but are happy to help you fill them.</p>
<p>I value the input and perspective that friends share with me. We don&rsquo;t have to agree on everything. We don&rsquo;t have to be interested in the same technologies. In fact both of those conditions help me understand things I wouldn&rsquo;t know about otherwise.</p>
<p>I cite my friends in my writing because they are important. They deserve credit when they teach me things. Processing ideas with my friends helps me test them for quality. Often they point out errors in my thinking. Those are all good things.</p>
<h2 id="be-curious">Be Curious</h2>
<p>This is one of the most important lessons I learned from <a href="/2023/12/goodbye-mom/">my mom</a>. She was a deeply curious person and that rubbed off on my sister and I. We both remember her dragging us to <a href="https://dfwi.rpi.edu/outreach/lecture-series">lectures at the RPI Freshwater Institute in Bolton&rsquo;s Landing, NY</a> (it&rsquo;s fun to discover those still happen – I highly recommend if you&rsquo;re in the area). And the time she took us to a ranger talk at a National Park where the ranger offered a postcard to anyone who asked a question he couldn&rsquo;t answer – she promptly earned all his postcards with questions she genuinely wanted answered. She would stop and talk to keepers at the <a href="https://www.philadelphiazoo.org">Philadelphia Zoo</a> to learn more about the animals (a habit my wife and I both now share).</p>
<p>Because of her constant interest in, well just about everything, I have been to historic sites, factory tours, art and science museums, zoos, and many other places. My wife – a curious person herself with her own curious parents – and I continue those kinds of adventures. We go places, we ask questions, and we learn things.</p>
<p>We also read widely, both in our professional fields, and about the world in general. Long drives often involve several hours of listening to podcasts featuring experts in a variety of fields. I&rsquo;ve previously talked about those as <a href="/2022/08/my-ongoing-liberal-arts-education/">my ongoing Liberal Arts education</a>.</p>
<p>If you aren&rsquo;t someone who was trained to be curious by your family, it turns out you can do things to <a href="https://bigthink.com/the-learning-curve/5-habits-increase-curiosity/">increase your curiosity</a>. And yes, I found that article cause I was curious to know if curiosity is trainable – there are <a href="https://link.springer.com/article/10.1007/s12144-022-03107-w">deeper articles on curiosity</a> out there too.</p>
<p>The value in being curious is that it causes you to learn things you might otherwise have missed. Some of those things just make you smarter about your world – and the world needs more smart people. Others will suddenly pop up in your work and make you better at your job. They might come up in random conversation with a new friend and deepen your connection to another person.</p>
<p>Learning new things is never a bad idea.</p>
<h2 id="always-learn-more">Always Learn More</h2>
<p>All this advice boils down to this central idea: <em>always being learning something new</em>. It&rsquo;s <a href="https://www.health.harvard.edu/mind-and-mood/train-your-brain">good for your brain</a> and good for your life (oh yeah, and career).</p>
<p>Reading through all of this commentary on professional development you might think: &ldquo;Wow, that takes a lot of time!&rdquo; But if you put yourself into a mindset of always trying to learn something new much of that times starts to overlap with other activities in life. And when it&rsquo;s not <em>all</em> dedicated directly to work it doesn&rsquo;t burn you out the way constant study of one technology of platform can.</p>
<p>Push yourself forward. Find things that are interesting to you. Nothing new is a waste of your time.</p>
]]></content:encoded> </item> <item>
      <title>My Self Documenting Code Doesn&#39;t Need to be in Git: and Other Lies Salesforce Developers Tell Themselves</title>
      <link>https://spinningcode.org/2025/06/my-self-documenting-code-doesnt-need-to-be-in-git/</link>
      <pubDate>
        Sun, 01 Jun 2025 00:00:00 +0000
      </pubDate> <guid isPermaLink="false">https://spinningcode.org/2025/06/my-self-documenting-code-doesnt-need-to-be-in-git/</guid>  <description>Developers love to lie to themselves. Herein you&amp;#39;ll find some of the worst lies Salesforce developers use on themselves.</description> <content:encoded><![CDATA[<p>One of the things that consistently surprises me as a developer in the Salesforce eco system are the lies developers are comfortable telling themselves. Some lies (like that their code is self-documenting) are common developer myths. Others (like not needing git) are Salesforce community specific.</p>
<p>So, as a public service to the community, I provide here a list of common lies and a sentence or two about why you should stop telling them.</p>
<h2 id="my-code-is-self-documenting">My code is self-documenting</h2>
<p>Not once in my career have I heard a developer say this and found their code to be &ldquo;self-documenting&rdquo;. Never. Nope. (If we&rsquo;re friends: yeah, not even yours.)</p>
<p>Developers who <em>claim</em> to write self-documenting code seem to solve problems in the least-obvious way available. Or at least it feels that way to me. Maybe if they left comments explaining <em>why</em> they used their approach it would seem brilliant. I don&rsquo;t know; it&rsquo;s never come up.</p>
<p>Self-documenting code doesn&rsquo;t mean following a variable naming convention and having <em>no comments at all</em>. You still need doc comments. You still need good functional decomposition. You still need comments to say <strong><em>why</em></strong> your code does things. Sure, I can usually figure out <em>what</em> it does, but not why.</p>
<p>If you still want to argue with me make sure you read <a href="https://web.eecs.umich.edu/~weimerw/2018-481/readings/mythical-man-month.pdf">Chapter 15 of Mythical Man Month</a> before calling.</p>
<h3 id="see-also-nobody-else-needs-to-understand-this-code-im-the-only-one-working-on-it">See Also: Nobody else needs to understand this code; I’m the only one working on it</h3>
<p>Just because you are the only one working on it right now, doesn&rsquo;t mean that will be true 4 years from now. I recently had to update code that had been running happily without attention for 5 years. By the time I was working on it, everyone had forgotten it <em>existed</em> let alone how it worked.</p>
<p>And don&rsquo;t forget <strong><em>you</em></strong> might be that person who needs to remember these details 5 years from now. Guess who I called first when we realized that code needed updating? Yup, the guy who wrote it. No, he didn&rsquo;t remember why he had made important choices either.</p>
<p>The only time I am <em>not</em> disappointed in the person who committed code in my name 6 months ago is when he wrote me really good comments to explain what he was thinking. When he didn&rsquo;t do that: that guy was an idiot.</p>
<p>Future you, your colleagues, friends, clients, strangers, they all will thank you for explaining yourself. Doesn&rsquo;t that feel better than having them all hate you?</p>
<h2 id="i-dont-need-to-use-git">I don’t need to use Git</h2>
<p><strong>Yes.</strong>
<strong>You.</strong>
<strong>Do.</strong></p>
<p>It’s 2025. Bell Labs <a href="https://en.wikipedia.org/wiki/Source_Code_Control_System">invented source control in 1972</a>. “I don’t know how to use it” isn’t a valid excuse. If you’ve been putting off learning to use these tools, it’s time you admit they aren’t going away.</p>
<p>Just because the code exists in a Salesforce sandbox org does <em>not</em> mean you don’t benefit from tracking its history, knowing who (including you) changed what when (and if <a href="https://xkcd.com/1296/">they wrote good comments</a> why!). The first time it saves you because another developer overwrote your code, it’ll have saved you all the time setup required and more.</p>
<p>I can setup a new repository, with Salesforce configuration, in 90 seconds. It takes 10 seconds to do a commit. Sure, you can have merge problems sometimes – that usually happens when two people make conflicting changes. When do you need source tracking the most? When two people make conflicting changes.</p>
<p>When you see two sandboxes with two different versions of code, how do you know which is right? On my projects it&rsquo;s the one that matches Git, cause Git is always canonical.</p>
<p>There really is no <em>good</em> argument against putting every line of code into a repo. Just because you <em>have</em> an argument does not mean you have a <em>good</em> argument. It&rsquo;ll take you less time to start using Git than it will take to argue with me.</p>
<h3 id="see-also-ill-just-comment-out-this-section-of-code-for-now-and-delete-it-later">See also: I’ll just comment out this section of code for now, and delete it later</h3>
<p>No you won&rsquo;t. Nope, stop kidding yourself. I&rsquo;ve seen your 200 line blocks of commented out &ldquo;stuff&rdquo; (without notes about why it was there in the first place, or why it&rsquo;s commented out). You want to keep it for 3 or 4 minutes while you test a change – okay sure. But this is part of what source control does for you. Deleted something you want back? Okay, go get it out of your source control system. Delete your block of commented out code before your next commit.</p>
<h2 id="i-will-fix-this-hard-coded-id-later">I will fix this hard-coded ID later</h2>
<p>If it&rsquo;s easy to fix, just do it now.</p>
<p>If it&rsquo;s going to be a bunch of work later, you know as well as I do that you&rsquo;ll find excuses not to bother at that point either.</p>
<p>Either write the query to get the Id you need now, or move it to custom metadata/setting/environment variable/whatever now. It&rsquo;ll take probably take 5 minutes to do it right now, and 20 minutes to <em>find</em> the stupid thing later (and 10 minutes to fix it cause you&rsquo;ll be tired from the 20 minute hunt).</p>
<h2 id="my-invocable-action-doesnt-need-to-be-bulkified">My invocable action doesn’t need to be bulkified</h2>
<p>Sure, damn near every example invocable action only processes the first record in the list. Yes, that encourages us to think it&rsquo;s okay to write code that way. And yes, bulkifying code is hard sometimes.</p>
<p>You might <em>think</em> that your record triggered flow will only need to deal with one user-entered update at a time. And then someone loads a 100,000 records and your action breaks the whole process.</p>
<p>And sure, you always <em>feel</em> like your action is only getting used in that one Screen Flow. But 9 months from now when someone realizes they can get the inputs other ways and call your action with the screens and puts it in a record triggered flow your code will still work.</p>
<p>There <em>exist</em> settings where the screen flow thing is true. They are rare, but I&rsquo;ve seen them. Use this measure to determine if it applies to your project: if you cannot write a rigorous proof which is good enough for your high school geometry/trig teacher (college CS or Math Professors will also count here) to accept showing this code will only ever be run in a one-record context – you better plan for 200 records.</p>
<h2 id="i-will-convert-this-test-setup-to-use-a-data-factory-when-i-have-more-tests">I will convert this test setup to use a data factory when I have more tests</h2>
<p>Why? Why would you think you will take the time to do this conversion later? When you arrive at “later” you will just need one more object added to your setup. Or maybe 2. And you can copy-paste it into the next test setup method. Then may add 2 more objects. And then <a href="https://www.neverjust.net/">you’ll just</a>…</p>
<p>If you don’t lay down good patterns up front, you won’t convert to them later. Starting a <a href="https://trailhead.salesforce.com/content/learn/modules/apex_testing/apex_testing_data">test factory class</a> doesn’t take a lot of time. Just do it now, and thank yourself later when you realize you’ve slowly built a factory that’s supporting 5 different test suites and hundreds of tests.</p>
<h2 id="ill-move-my-business-logic-out-of-the-trigger-once-its-working">I’ll move my business logic out of the trigger once it’s working</h2>
<p>Once your code is working you have almost no incentive to clean it up. Chances are you&rsquo;ll just deploy your code and wash your hands of it. It wont be until you&rsquo;re neck deep in a conflicting sets of requirements that you&rsquo;ll decide it&rsquo;s time to refactor this mess – and then you&rsquo;ll wonder what each conditional was for in the first place (see self-documenting code above).</p>
<p>Even during a recent technical interview, I took the 15 seconds needed to create the trigger and have it call a service class. I didn&rsquo;t plan to keep that code for more than 15 minutes and I still bothered. I really think it took 15 seconds. Bing, bang, done. Sorting it out later is so much harder then just doing it on the first pass.</p>
<h2 id="as-longs-as-i-hit-the-test-coverage-requirement-my-tests-are-good-enough">As longs as I hit the test coverage requirement, my tests are good enough.</h2>
<p>Automated test coverage is not about hitting a certain number of lines run – that&rsquo;s the minimum threshold. Good automated tests ensure you code works <em>correctly</em>. Good test coverage now means that when you revise the code later you know if your revisions broke something.</p>
<ul>
<li>Do not play games with getting the coverage percentage up.</li>
<li>Do not have tests with no assert statements.</li>
<li>Do not decide that 75% is just fine because to get to 90% you&rsquo;d have to write three more tests – write the three tests!</li>
<li>Do not let test coverage meaningfully changes when someone auto-formats your code.</li>
</ul>
<p>Write tests that ensure your code meets specifications, not tests that make the deployment process happy.</p>
<h2 id="i-dont-need-automated-code-standards-enforcement">I don’t need automated code standards enforcement</h2>
<p><a href="https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview">Salesforce Code Analyzer</a> is a free tool you can install right now and will help ensure you don’t do stupid stuff in your code. It will also make sure you team follows the same habits. These tools have been around for Salesforce developers for years, but for some reason we don’t all consistently turn them on.</p>
<p>This is standard fare for professional developers. What makes you special that you don&rsquo;t need to follow standards? Why would you think you can enforce standards on a team without automation?</p>
<p>Every time I have to do a commit to reformat code to be consistent with all the other code on a project, a [insert sad thing here] happens. This pretty much only comes up with people who don&rsquo;t have auto-formatting of their own. Their code had blends of tabs and spaces, different line endings within the file, off-by-one spacing error (dear God I hope no one uses 3 on purpose), and all kinds of things that cause other editors to sputter and complain.</p>
<p>If you don&rsquo;t like Salesforce&rsquo;s default analyzer rules, you can setup your own. It&rsquo;s just enforcing ESLint and PMD patterns. I have extremely little interest in debating code standards – <em>just have some!</em></p>
<h2 id="special-thanks-for-shared-salesforce-developer-lies">Special Thanks For Shared Salesforce Developer Lies</h2>
<p>The following people sent me lies to include in this list, and I appreciate their shared pain. My rants about each are mine alone.</p>
<ul>
<li><a href="https://www.linkedin.com/in/peter-churchill-bfc/">Peter Churchill</a></li>
<li><a href="https://www.linkedin.com/in/michelleelizabethhansen/">Michelle Hansen</a></li>
<li><a href="https://www.linkedin.com/in/raul-iii-roque/">Raul Roque</a></li>
</ul>
]]></content:encoded> </item> <item>
      <title>Writing for Developers and Consultants: Guides and Resources</title>
      <link>https://spinningcode.org/2025/05/writing-for-developers-and-consultants-guides-and-resources/</link>
      <pubDate>
        Sat, 31 May 2025 00:00:00 +0000
      </pubDate> <guid isPermaLink="false">https://spinningcode.org/2025/05/writing-for-developers-and-consultants-guides-and-resources/</guid>  <description>Some guides and suggestions to improve your writing and communication skills as a developer or consulting.</description> <content:encoded><![CDATA[<p>Last year I wrote several pieces on writing for Developer and Consultants (the <a href="https://spinningcode.org/category/writing">writing category</a> will bring them up). That series became a talk on broader communications skills that I gave at Dreamforce 2024, and other settings – including twice this month. Part of that talk includes a series of books and similar resources that weren’t part of the original series. I figured it was time to add them in here.</p>
<p>I want to be clear, these are not the <em>only</em> resources to help you be a better communicator. They are just <em>some</em> resources that I have found useful and I hope you might also find useful.</p>
<h2 id="basics">Basics</h2>
<p>I generally list two works to help with the basic mechanics: <a href="https://www.gutenberg.org/files/37134/37134-h/37134-h.htm"><em>Elements of Style</em></a> and the <em><a href="https://archive.org/details/artofpublicspeak00esenrich">Art of Public Speaking</a></em>. Both are over a century old, and yet still commonly referenced and both still have useful advice.</p>
<h3 id="elements-of-style">Elements of Style</h3>
<p>Commonly referred to as “Struct and White”, <em>Elements of Style</em> is a short and easy to consume guide to writing well in formal English. It’s an excellent starting place for improving formal writing. But it is just that: a starting place. It is committed to helping the readers understand the formal rules – the elements. <em>Elements of Style</em> is not committed to teaching style, or even encouraging your own personal voice. For those things you will need to explore further.</p>
<h3 id="art-of-public-speaking">Art of Public Speaking</h3>
<p><a href="https://en.wikipedia.org/wiki/Dale_Carnegie">Dale Carnage</a> wrote a series of guides in the first half of the twentieth century to help people (mostly men) perform better in white collar work places. He started almost accidentally with the <em>Art of Public Speaking</em>. Many of his guides are still commonly read and referenced – almost to the point of being cliché (<em><a href="https://en.wikipedia.org/wiki/How_to_Win_Friends_and_Influence_People">How to Win Friends and Influence People</a></em> is also his). While his works are certainly somewhat dated at this point, the basic instructions on how to use your voice, organize your thoughts, and even the practice exercises it includes are still extremely useful when working on how to improve your speaking presence.</p>
<h2 id="further-explorations">Further Explorations</h2>
<p>As you gain comfort with the formal rules and techniques, you may find you struggle to find your own voice within those rules. The good news is that you don’t actually have to follow those rules all the time – <a href="https://spinningcode.org/2024/05/writing-for-developers-and-consultants-use-of-language/">just when it suits your needs</a>. But learning how to manipulate the rules gives you control and freedom over the language. You might also find you want tools beyond just words to accomplish your goals.</p>
<h3 id="test-the-limits-of-grammar">Test the Limits of Grammar</h3>
<p>Lynn Tross’s <a href="https://www.amazon.com/Eats-Shoots-Leaves-Tolerance-Punctuation/dp/1592402038">Eats, Shoots &amp; Leaves</a> helps with the first of those two goals. She has an entertaining approach to looking at how and why punctuation matters. But she also makes it clear that some uses are about style as much as clarity. She encourages her reader to both take punctuation seriously and to have fun with it. With her insights you can think about how to bend, twist, and even break the rules outlined in guides like Stuck and White. I know several people who found her guidance helped them take more control over all parts of their writing.</p>
<h3 id="dont-only-use-words">Don’t Only Use Words</h3>
<p><a href="https://www.youtube.com/watch?v=84lVphpwFYg">Dan Roam’s</a> <a href="https://www.amazon.com/Blah-What-When-Words-Dont/dp/1591844592/">Blah, Blah, Blah</a> provides ideas for how to handle the second of those challenges: what to do when words alone don’t work. It provides an overview of why and how to use images to help improve your presentations and writing. Like all the others it can give you ideas and inspiration. Where you go from there should be your own interpretation of those ideas and suggestions.</p>
<h2 id="keep-going">Keep Going</h2>
<p>Reviewing materials that are meant to train you in basic techniques is necessary, but not sufficient. You need to take the time to see how other experts use those techniques in practice. The good news is that just about anything on a technical topic can be helpful here.</p>
<h3 id="more-readings">More Readings</h3>
<p>For reading on technical issues I tend to personally suggest well known works. Books because well know, because they are good (usually). Books stay important because they have important ideas – even if we have moved away from the details.</p>
<p><a href="https://archive.org/details/mythicalmanmonth00broo">Mythical Man Month</a>, for example was often required reading for developers and project managers for decades. While we want to believe our field advances so fast that a book first written in the late 1960’s is ancient history, the content is surprising relevant. It has held on so long because Fred Brooks covered important ideas and because he expressed them well.</p>
<h3 id="speeches-and-keynotes">Speeches and Keynotes</h3>
<p>Listening to speeches, watching videos of great talks can be excellent as well. The obvious choice here is to watch <a href="https://www.ted.com/">Ted Talks</a>. The only down side to those is they can be intimidatingly good. They have an extremely high production quality, world renowned speakers, and a well trained team to make it all happen. While I find that content excellent and inspiring, I would never try to match their production quality for a talk I’m giving at a local user group meeting.</p>
<p>For that I look to more simplistic content. When I commuted by train many years ago I would listen to talks published on the <a href="https://archive.org/details/conversationsnetwork_org">old Conversations Network</a> because they were interesting, audio only (so good for public consumption), and sometimes flawed – I could imagine myself giving talks like that. More recently I’ve taken up listening to podcasts from the <a href="https://freakonomics.com/podcasts/">Freakonomics Radio Network</a> and other places that are polished and high quality, but not so much that you can’t imagine reaching their level. Particularly shows outside their flag-ship Freakonomics are a little rougher around the edges while still being a good listen with interesting ideas.</p>
<h2 id="practice">Practice</h2>
<p>All of these resources are only useful if you put them to work. I’ll talk about this more in a future article, but to really get good at this you’re going to need to practice. Nearly everything in life improves through practice, including your communication skills. To really get good at this, plan to put in some work. And expect to fail sometimes. It’s how we all learn and grow.</p>
<p>As I said in <a href="/2024/02/writing-for-developers-and-consultants-editing/">my first post on this topic</a>, communications skills for developers and consultants is an enormous topic. The plan for this series is evolving as I go. If you have suggestions or requests feel free to leave me a message.</p>
]]></content:encoded> </item> <item>
      <title>Salesforce Id Iteration Attacks</title>
      <link>https://spinningcode.org/2025/04/salesforce-id-iteration-attacks/</link>
      <pubDate>
        Thu, 10 Apr 2025 00:00:00 +0000
      </pubDate> <guid isPermaLink="false">https://spinningcode.org/2025/04/salesforce-id-iteration-attacks/</guid>  <description>Please stop using Salesforce Ids in URLs or other user accessible parts of your solution.</description> <content:encoded><![CDATA[<p>Please stop using Salesforce Ids in URLs or other user accessible parts of your solution.</p>
<p>Why not use Salesforce Ids on public web sites? I run into this question from time to time and figured some more public commentary would be useful.</p>
<p>The super short version: they are not a security or access control mechanism.</p>
<p>During a recent Salesforce Open Source Commons sprint a question came up about why it’s important to use a random value, instead of a Salesforce Id, in public facing use cases. We were talking about the <a href="https://github.com/SFDO-Community/UnsubscribeLink">Unsubscribe Link</a> package, and it makes a good use case for discussion.</p>
<h2 id="what-is-unsubscribe-link">What is Unsubscribe Link?</h2>
<p>Here is some quick context on Unsubscribe Link and why I’ll be using it as an example in this post. <a href="https://github.com/salgsstyrke">Jessie Rymph</a> created the Unsubscribe Link package for Salesforce Labs. Community volunteers are taking over the future of that package and those volunteers need to speed on the great work she already did.</p>
<p>The package provides links you can embed in email messages sent by Salesforce that allow recipients to unsubscribe from future messages. These links need to allow the recipient, and just the recipient, to make this update. It needs to update the data in Salesforce in a secure manner, based on a link provided in an insecure channel (an email message). Links need to be reliable and consistently formatted, but not guessable. The package, rightly, uses UUIDs in those link to match the link opened back to the correct Salesforce record.</p>
<h2 id="the-attack-on-salesforce-ids">The Attack on Salesforce Ids</h2>
<p>If you have a public facing web page or service that allows for display or update of data that filters access based purely on the Salesforce Id, you are at risk. An attacker can manipulate those Ids and either steal, or mass update, your data. This is called a “bad thing”.</p>
<p>The technical terms here are: <a href="https://cheatsheetseries.owasp.org/cheatsheets/Insecure_Direct_Object_Reference_Prevention_Cheat_Sheet.html">Insecure Object Reference</a> leading to a <a href="https://owasp.org/www-community/attacks/Web_Parameter_Tampering">Web Parameter Tampering</a> attack.</p>
<p>In the case of Unsubscribe Link, if the link used the Salesforce Id, it would be possible to unsubscribe all, or nearly all, Contacts in the org. The attacker would start with the Id in the email they got, and then start scanning up and down the Id range (see next section for how). Unsubscribe Link is protected against this attack, but I see elsewhere all the time.</p>
<p>Here are a couple quick examples (which I saw, helped fix, and hear people describe recreating):</p>
<ul>
<li>An org that allowed members to confirm their details by clicking a link in an email. They could then see their contact and membership information on a web page. That web page had no authentication, and the link’s only parameter was the Salesforce Id. Once you had one link, you could change Id and view other people’s data.</li>
<li>An event check in solution that used a QR code volunteers scanned at the door. The QR code contained a link that lead to a form which showed their registration information (including name and email) and updated their Campaign Member status – again the only link parameter was the Salesforce Id. Anyone with the QR code could have pulled the complete email list of attendees with a few hours of scripting.</li>
</ul>
<h2 id="salesforces-id-structure">Salesforce’s Id Structure</h2>
<p>To understand how an attacker can do this it is useful to understand <a href="https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/field_types.htm#i1435616">Salesforce Ids</a>. They often look random but are not. They are defined by a predictable, and published, pattern. If you want <a href="https://codebycody.com/salesforce-ids-explained/">really gritty detail have a look at this post</a>. But I’ll give you the short version here.</p>
<p>They start with a 3 character prefix that encodes the object type. Salesforce provides documentation for decoding the id prefix for all standard objects. Custom objects have a prefix that’s specific to your org.</p>
<p>The next three characters are an instance reference. This reference indicates the Salesforce instance hosting your org when creating the record. It’s highly stable, but <em>can</em> change from time to time.</p>
<p>Then there is a zero that’s reserved for future use.</p>
<p>Next comes a 8-digit base-62 number. This provides a huge range, but it is just an incrementor stored in a convenient fashion.</p>
<p>That covers the first 15 characters. Salesforce Ids come in two flavors, 15 or 18 characters. These 15 character Ids are case-sensitive base-62 strings. In most cases you will be using the 18 character flavor. The 18 character version are case-safe and add three more characters that encode the case of the preceding 15 characters. So those last three also often <em>look</em> random, but are entirely calculable based on the first 15.</p>
<p>From an attackers perspective a Salesforce Id looks like this:</p>
<p><code>[7-digits I can copy and paste][a very big number][three digits I calculate]</code></p>
<h3 id="how-do-they-know-what-salesforce-ids-to-scan">How Do They Know What Salesforce Ids to Scan</h3>
<p>For an attacker the only hard bit is figuring out what very big numbers to use.</p>
<p>These Ids are not meant to be random or secure. You should think of an 18-digit as a base-62 number. Worse yet, an attacker only needs to scan a small portion of the space to find data. Only 8 digits are actually incrementing (still a huge range – 62^8 is large by even astrophysics standards).</p>
<p>If an attacker had to check that whole range they’d never find you. But your Ids are clustered together relatively tightly. Worse yet, when they start their scan, they already know where to look – you told them.</p>
<h3 id="but-i-read-this-isnt-important-in-practice">But I Read This Isn’t Important In Practice!</h3>
<p>There is commentary around suggesting that these numbers are too big to guess so they therefore protect you from attack. If those numbers were random that might be true, but they aren’t randomly assigned.</p>
<p>Salesforce generates Ids sequentially within a node which is often hosting multiple customers. The node started at zero once upon a time. Every time a record was created Salesforce incremented that number. Whenever someone runs tests, it increments the number and then dumps the data (and those Ids). When someone on another org on that instance adds data, up goes the number. During bulk data loads there are multiple processes running, the processes will each reserve a transaction’s worth of Ids at a time and may be on different parts of the file making them look out of order. If one of those transactions fails, it’ll create a gap in the sequence (typically 200 records in length).</p>
<p>All that means that your attacker usually cannot reliably guess the Id of a specific record, but if they scan large numbers of records they can find lots of data.</p>
<p>Those behaviors do spread the data out over a slightly larger area than a purely sequential Id in a traditional database. But your data all starts are some number with Ids above that number. The larger your org, and more active you are, the more likely it is that your data has dominated Id generation since you joined the node.</p>
<h3 id="yeah-they-know-how-to-attack-you">Yeah, They Know How to Attack You</h3>
<p>You have also done things in the course of business that clustered your Ids together in sub-ranges that help your attacker. For example when you migrated data into your org, or any other bulk load, you created a lot of records with very tightly clustered Ids. So while searching they may find large gaps between records, and long runs of perfectly sequential Ids.</p>
<p>If the attacker knows one Id, then they know what area to start looking around in for more. How do they get that first Id? I already told you: you gave it to them.</p>
<p>In my experience an attacker is going to find your vulnerable service by getting one of the links. They will quickly see that it doesn’t require authentication, and that the Salesforce Id is the primary parameter. That link has an Id in it. That means they have an Id in their hands from whatever email or other solution you used. They don’t have to start at zero and count up until they find your range. They aren’t picking a random starting point. Instead they will start with a number they know is in your range. After that they can just search up and down the number line to find a lot more.</p>
<p>Sure they will get a lot of misses – but who cares? Attackers don’t do this by hand, they use software that works fast and never gets bored. They also know things about the likely size of gaps, can spot common patterns, and hop around in smart ways to find likely valid Ids. Attackers spend a lot of time thinking about these problems. Always assume they know patterns that you did not know existed.</p>
<h2 id="solutions">Solutions</h2>
<p>There are two main ways to prevent these attacks:</p>
<ol>
<li>Use authentication for all web services – even when that’s hard or annoying.</li>
<li>Use a truly random value to do the lookup and limit the attacker to one record.</li>
</ol>
<p>If you put some form of reliable authentication in place you can make this problem largely go away. But it limits what you can do. That can work for event registration or member directories – but it doesn’t work for Unsubscribe Link or other unauthenticated use cases.</p>
<p>A properly generated UUID creates a random value that cannot be guessed or iterated. Unsubscribe Link <a href="https://github.com/search?q=repo%3ASFDO-Community%2FUnsubscribeLink%20path%3A%2F%5Eforce-app%5C%2Fmain%5C%2Fdefault%5C%2Fclasses%5C%2F%2F%20UUID&amp;type=code">leverages this approach</a> to make it impossible to unsubscribe another person. The package assigns each Contact or Lead a UUID generated with <a href="https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_class_System_UUID.htm">Apex’s UUID class</a>. The package then uses that UUID in the link included in the email. When someone clicks that, it opens a page which searches for the UUID and updates the matching record. It doesn’t have to be a UUID, there are other solutions to get truly random data, but UUIDs are a great solution in lots of use cases.</p>
<p>Depending on the setup, you can also create or use services to detect these kinds of attacks. It’s not uncommon to see but not something Salesforce provides directly, nor is it common to see with form providers and other services.</p>
<p>In the end, secure design is your responsibility.</p>
]]></content:encoded> </item> <item>
      <title>Experiments with AI Coding</title>
      <link>https://spinningcode.org/2025/03/experiments-with-ai-coding/</link>
      <pubDate>
        Sat, 22 Mar 2025 00:00:00 +0000
      </pubDate> <guid isPermaLink="false">https://spinningcode.org/2025/03/experiments-with-ai-coding/</guid>  <description>&lt;p&gt;I recently spent some time generating some simple programs with AI coding tools. As a technical architect and senior developer, I am watching the emergence of AI coding tools carefully. I know that the robots are coming for the coding parts of my job. Watching this generation of robots is important to long-term understanding of the field.&lt;/p&gt;
&lt;p&gt;To be clear I have been experimenting with &lt;a href=&#34;https://github.com/features/copilot&#34;&gt;Github Copilot&lt;/a&gt; and &lt;a href=&#34;https://developer.salesforce.com/blogs/2024/09/introducing-agentforce-for-developers&#34;&gt;Salesforce’s Agentforce for Developers&lt;/a&gt; since they were released. Most of my earlier experiments were failures. The tools wrote trivial code terribly and simply couldn’t understand more complex requests. They made me slower, not faster, so I put them aside for a time. But they are evolving fast and were worth another look.&lt;/p&gt;</description> <content:encoded><![CDATA[<p>I recently spent some time generating some simple programs with AI coding tools. As a technical architect and senior developer, I am watching the emergence of AI coding tools carefully. I know that the robots are coming for the coding parts of my job. Watching this generation of robots is important to long-term understanding of the field.</p>
<p>To be clear I have been experimenting with <a href="https://github.com/features/copilot">Github Copilot</a> and <a href="https://developer.salesforce.com/blogs/2024/09/introducing-agentforce-for-developers">Salesforce’s Agentforce for Developers</a> since they were released. Most of my earlier experiments were failures. The tools wrote trivial code terribly and simply couldn’t understand more complex requests. They made me slower, not faster, so I put them aside for a time. But they are evolving fast and were worth another look.</p>
<p><a href="/2019/12/on-being-self-taught/">I learn by doing.</a> So I set about creating a few tools that were on my mind to see if I could get good code out of the LLMs. Or at least if they could save me enough time to be worth the learning curve to get good at using them.</p>
<h2 id="project-1-simple-openwrt-status-call-out">Project 1: Simple OpenWRT Status Call Out</h2>
<p>I recently updated my home wifi router to run <a href="https://openwrt.org/">OpenWRT</a>. When I bought the router I selected one compatible with that OS. After realizing T-Link would never fix some security issues in the firmware I made the switch.</p>
<p>Our internet service from <a href="https://www.breezeline.com/">Breezeline</a> has always been spotty; sometimes bad enough I need to relocate to get stable service. One of the things I want to know when I’m working elsewhere is: has my internet come back up yet?</p>
<p>I have a programmable router, I’m a programmer, therefore this is a problem I could solve for myself.</p>
<p>But I’ve never written code to work with OpenWRT. Worse, I generally work in high-storage environments. I mostly do side projects in Python or NodeJS, but my router doesn’t have the storage needed for those run-times. OpenWRT recommends C or Lua for scripting. I’ve done my time in C. Jumping through all the hoops required to make C do something this simple did not feel like fun. So Lua it is. Too bad I hadn’t written code in Lua before…enter Copilot.</p>
<p>Throughout this project I used the current defaults – which meant GPT-4o was the selected LLM.</p>
<h3 id="initial-code">Initial Code</h3>
<p>I actually found Lua by asking Copilot. My first prompt was just:</p>
<blockquote>
<p>Generate a program for openwrt that sends a post request to an API endpoint at spinngingcode.org</p>
</blockquote>
<p>And it generated a reasonable looking Lua script. Since I don’t know Lua, or at that point why Copilot chose it, I dove into a little research into the language. After a few minutes I confirmed that, yes, this is the right use case for Lua. I also confirmed Copilot made reasonable choices in how the code was written.</p>
<p>I went on to ask Copilot to change call outs from curl to a library, add features to pull in the network status information from the router, and other adjustments. It consistently generated code that was close to correct. I did have to tell it to adjust details after validating them against my router’s actual setup and commands. It took some time but on the whole I was able to make steady progress. Eventually I had a good-enough solution.</p>
<h3 id="tests">Tests</h3>
<p>Automated test generation has been a promise of these tools since they arrived. It always feels like something that’s easy to do: read code, write test. Which is, of course, the 100% wrong way to write tests. However, the point of the exercise was to see what I could get the tools to do, so I gave it a try.</p>
<p>It took several tries for it generate valid tests. The first time I asked, it wanted to know what framework to use (a fair request), but generated a useless <code>hello_world</code> test file. On subsequent tries it switched frameworks without asking, and took several tries before generating something that looked valid.</p>
<p>Too bad it <em>wasn’t</em> valid.</p>
<p>The test framework I picked, <em>luatest</em>, was a bad choice. But with little experience in the language I didn’t know that. When I finally had it refactor to <em>busted</em>, I got tests I could setup and run…and they all failed cause they were still all wrong.</p>
<p>I spent more time getting a valid test setup than primary code – eventually I gave up. It would have been faster to do the research and figure it out from scratch.</p>
<h3 id="code-review">Code Review</h3>
<p>All in all, Copilot gave me a reasonable piece of code for the primary request. Using Copilot was far faster than I could have done the research to write something equally good. It still needs a lot of supervision to get it right, but the refactoring requests seemed to go okay.</p>
<p>The code commentary Copilot provided in the response is detailed and accurate, where I checked it. It’s chatty so I didn’t check every piece of commentary just every line of code. The code itself came with passable comments about what each block and function does. And functional decomposition was vastly improved over previous experiments I’ve done with Copilot.</p>
<p>It had to make up a few details, like the actually API endpoint name (it picked <code>https://spinningcode.org/api/endpoint</code> – okay sure reasonably good fake answer that is clear but also obviously wrong), but those were good enough for the limited details I gave it.</p>
<p>Outside of the automated test disaster, the biggest issue was that it did <em>nothing</em> to encourage secure design. Copilot recommended no security for that endpoint, nor does the commentary flag that it skipped security suggestions.</p>
<h2 id="project-2-simple-php-status-server">Project 2: Simple PHP Status Server</h2>
<p>Since I have PHP on my personal server, I decided that PHP was the right choice for the server side of the router project. Despite being a little rusty I am much more comfortable with PHP than Lua. I have standards and expectations about how to write and organize good PHP code.</p>
<p>The prompt I provided:</p>
<blockquote>
<p>I have a Lau script that looks like this:</p>
<p>[a copy of the then current version of the Lua file]</p>
<p>Now I need a PHP application that will listen to those calls, record the data into a CSV file, and includes a page that displays the last 100 lines of the csv file.</p>
</blockquote>
<p>It managed to generate valid PHP code that matched my request – sorta.</p>
<h3 id="code-review-1">Code Review</h3>
<p>The PHP it generated is not to my standards – not even close.</p>
<p>It lacked security checks, was poorly organized, and had borderline useless comments. If you <a href="https://github.com/acrosman/openwrt-status-message/tree/main/server">look at the repo</a> there are both a lot of generated refactors and then a lot of hand edits cause that’s web facing code, security matters, and I could fix it faster than debating with an AI.</p>
<p>I made some effort to get it to highlight the important information I wanted, but it struggled to regain the context from the OpenWRT requests that the Lua script was built around.</p>
<p>Unlike the Lua script, the PHP saved me little or no time. It was terrible in this context even with lots of attempts to refine the code. This piece of code I’ll probably maintain by hand.</p>
<h2 id="project-3-data-migration-scripts-for-salesforce">Project 3: Data Migration Scripts for Salesforce</h2>
<p>One of the hardest things to get people to fund sufficiently for a Salesforce project is the data migration. They are hard, boring (well not for everyone – I have undying respect for people who like these projects), and super time consuming. So we are always looking for ways to do them faster.</p>
<p>A few years ago I proposed a training exercise for data migrations that involved taking the data from my <a href="https://github.com/acrosman/sc_salary_data">SC Salary Data repo</a> and loading it into Salesforce. It’s a great training project because there is lots of data and it’s real-world messy. But it’s also simple: just two or three objects depending on the data model you choose. So I decided to test if I could use AI code to generate basic scripts to clean and load this data quickly.</p>
<p>For this project I made two changes to my approach. First, I switched the model to Claude 3.5. Second, I decided to treat the LLM like a junior developer and lead it through the project more intentionally. Part of this second change was driven by my initial experiences with the other projects. I assumed from the start that the code would be sub-standard, but that I could encourage Copilot to make changes to meet my expectations – just like I do when working with a human.</p>
<h3 id="code-review-2">Code Review</h3>
<p>This project was startling successful. The first stage was to have it create a <a href="https://github.com/acrosman/sc_salary_data/blob/master/create_sqlite3_database.py">script that prepped all the data into a simple SQLite3 database</a> (it does some clean up, but not all that’s possible with this data). The second stage was to have it create a script to load the data. That <a href="https://github.com/acrosman/sc_salary_sf_loader">second stage became it’s own repo</a> because I wanted to include a Salesforce SFDX project to setup the objects and fields I wanted correctly.</p>
<p>I generated probably 90+% of the code in <a href="https://github.com/acrosman/sc_salary_sf_loader">that repo</a> with the LLM. The Python to load the data, most of the Salesforce Metadata to create objects and fields needed, even the read me file I generated in large part with the LLM.</p>
<p>Frankly this is a project I’ve seen developers struggle to get right, and with a little guidance the LLM was able to do it. It probably needed as much guidance as a human would have (a very junior human developer), but the iteration cycles were much faster. I am not convinced when all is said and done the project was faster – it took a few hours to complete each stage – than my doing it solo. However, it required far less mental engagement from me than if I had written the code unassisted.</p>
<p>This project went well enough I’m using it to help drive a conversation about how to change our approach to migrations in general. There are major differences between what I did and I do in a migration of a database with thousands of tables targeting hundreds of objects with millions of records. But I was impressed enough to want to move on to fill those next gaps.</p>
<h2 id="conclusions">Conclusions</h2>
<p>LLM generated code is improving fast, but it is still generating low-quality code by default. Unlike experiments I ran a year ago, it now generates functional code that is equal to the code that a brand new developer could write.</p>
<p>Generated code is not to be trusted, particularly in production or public facing settings. If you don’t ask for security, you don’t get security. It organizes code as if it’s following train of thought, not in the order that well structured code should follow. And it does not write tests worth having without significant effort.</p>
<p>Good AI driven tools can make good developers faster. They are not good enough to replace a good developer – yet. Developers will need to learn to work these tools into the process, but we need to be aware of the gaps and how to close them.</p>
<p>I expect the quality also depends deeply on the quality of the code in public repos on the internet. I believe the terrible quality of the PHP I got vs the reasonably good Lua and Python is because there is lots of terrible PHP out there used to train these models. As long the LLMs write code that conforms to the average of the examples online communities will need to sweat the details of what is out there.</p>
]]></content:encoded> </item> <item>
      <title>Reset Security Token for Salesforce Integration User</title>
      <link>https://spinningcode.org/2025/02/reset-security-token-for-salesforce-integration-user/</link>
      <pubDate>
        Sun, 16 Feb 2025 00:00:00 +0000
      </pubDate> <guid isPermaLink="false">https://spinningcode.org/2025/02/reset-security-token-for-salesforce-integration-user/</guid>  <description>&lt;p&gt;In the &lt;a href=&#34;https://help.salesforce.com/s/articleView?id=release-notes.rn_api_integration_license.htm&amp;amp;release=242&amp;amp;type=5&#34;&gt;Spring ’23 release&lt;/a&gt; Salesforce added the Integration User License. They provide 1 or more free licenses to all Enterprise, Unlimited, Performance, and Developer Edition orgs. They can be a &lt;a href=&#34;https://help.salesforce.com/s/articleView?id=platform.integration_user.htm&amp;amp;type=5&#34;&gt;little tricky to setup&lt;/a&gt;, but are incredibly useful for integrations since they give you a free way to connect 3rd party applications where “best practice” had previously required buying a full license for each integration point.&lt;/p&gt;
&lt;p&gt;But because they have no user interface access there is no clear path to reset the security token (in fact Salesforce says you cannot do it, although it’s &lt;a href=&#34;https://ideas.salesforce.com/s/idea/a0B8W00000GdbfuUAB/api-only-profile-users-should-receive-security-token&#34;&gt;listed as on Roadmap&lt;/a&gt;). There is a way to get this done, it just requires that you have &lt;a href=&#34;https://help.salesforce.com/s/articleView?id=xcloud.logging_in_as_another_user.htm&amp;amp;type=5&#34;&gt;Login As permission&lt;/a&gt;.&lt;/p&gt;</description> <content:encoded><![CDATA[<p>In the <a href="https://help.salesforce.com/s/articleView?id=release-notes.rn_api_integration_license.htm&amp;release=242&amp;type=5">Spring ’23 release</a> Salesforce added the Integration User License. They provide 1 or more free licenses to all Enterprise, Unlimited, Performance, and Developer Edition orgs. They can be a <a href="https://help.salesforce.com/s/articleView?id=platform.integration_user.htm&amp;type=5">little tricky to setup</a>, but are incredibly useful for integrations since they give you a free way to connect 3rd party applications where “best practice” had previously required buying a full license for each integration point.</p>
<p>But because they have no user interface access there is no clear path to reset the security token (in fact Salesforce says you cannot do it, although it’s <a href="https://ideas.salesforce.com/s/idea/a0B8W00000GdbfuUAB/api-only-profile-users-should-receive-security-token">listed as on Roadmap</a>). There is a way to get this done, it just requires that you have <a href="https://help.salesforce.com/s/articleView?id=xcloud.logging_in_as_another_user.htm&amp;type=5">Login As permission</a>.</p>
<ol>
<li>Go to Setup -&gt; Users, and find the Integration user for which you need to reset the security token.</li>
<li>Click Login next to the user.</li>
<li>You will be logged in as this user (it used to be that you were using the <a href="https://help.salesforce.com/s/articleView?id=xcloud.lex_aloha_comparison.htm&amp;type=5">Salesforce Classic Interface</a> but that’s no longer true). You can <a href="https://help.salesforce.com/s/articleView?id=xcloud.user_security_token.htm&amp;type=5">reset the token using the standard process</a>.</li>
</ol>
<p>And from there Salesforce should email you the new token.</p>
<p>It is worth noting this is not best practice. Best Practice is to use the OAuth JWT Bearer flow or the OAuth Client Credentials which don’t require the tokens. But not all tools are setup to use those flows, so the tokens are still useful.</p>
<p>When Salesforce completes providing a direct mechanism it should be better than this process, but until then you can happily reset tokens as needed.</p>
]]></content:encoded> </item> <item>
      <title>Architectures for Constituent Portals Part 2</title>
      <link>https://spinningcode.org/2025/01/25/architecture-for-portals-2/</link>
      <pubDate>
        Sat, 25 Jan 2025 12:00:00 +0000
      </pubDate> <guid isPermaLink="false">https://spinningcode.org/2025/01/25/architecture-for-portals-2/</guid>  <description>To make the right selection you need to balance your overall data ecosystem, staffing, budget, audience interest, as well as projected growth over time. I keep wanting to create a simple decision matrix, but an easy solution eludes me still.</description> <content:encoded><![CDATA[<p>In <a href="/2024/11/30/architecture-for-portals">part 1</a> I talked about the kinds of architectures you should considering when planning a Constituent portal. In part 2 I’ll give you some guidance about how to pick the right architecture for your project.</p>
<h2 id="reminder-of-the-options">Reminder of the Options</h2>
<p>Part 1 provides a detailed review of the directions that data can flow, and the types of architectures you can use to support your solution:</p>
<ul>
<li>Data can be outbound to constituents, provided by individual constituents, or exchanged among constituents.</li>
<li>There are all-in-one solutions, two-system solutions, and solutions with an API or data proxy layer.</li>
</ul>
<p>Three patterns, three potential solutions: it’s temping to think they align 1:1 – but then a whole second article would have been a waste of time. But it is important to keep them in your mind as you determine the best-fit for your organization and use case(s).</p>
<h2 id="portal-architecture-considerations">Portal Architecture Considerations</h2>
<p>To make the right selection you need to balance your overall data ecosystem, staffing, budget, audience interest, as well as projected growth over time. I keep wanting to create a simple decision matrix, but an easy solution eludes me still.</p>
<h3 id="audience-engagement">Audience Engagement</h3>
<p>The first major consideration is how will your audience actually engage with the portal. This should drive not only how you build it, but if you build it at all. Lots of organization believe their constituents are all waiting for a great portal to see all their information, update their addresses, and even communicate with each other. This is rarely true. None of us is short on things to do online – you need to be clear about what makes your portal worth their time.</p>
<p>When you align your project to your audiences interest, a portal is powerful, useful, tool that can make a huge impact. But if your audience isn’t interested in what you offer them, you just spent a bunch of money on a web site no one uses. There are times when the most effective constituent portal is none at all – because you can use those resources elsewhere.</p>
<p>Think carefully about how many of you constituents want to log into your portal enough to:</p>
<ol>
<li>Remember it’s there.</li>
<li>Care enough to take the time to log in.</li>
</ol>
<p>You spend something like 40 hours (probably more) a week thinking about your organization’s work. They spend 40 hours (or more) a week thinking about their employer’s work, also their families, friends, all the companies and nonprofits they interact with, politics, meal planning, shopping, and whatever else – there isn’t much time left for your organization.</p>
<p>Think about the features you can provide that are valuable to your audience and the best way to deliver those. For example, organizations often want to give people places to update their address or payment information. But think about the last time you moved, did you want another place to change your address information? Another common ideas is that nonprofits want to make delivery of annual tax receipts cheap and easy. If successful that means a wave of use once a year that might save your on mailing costs – of course you can also ask for a gift in that mailing so it might not actually be a net gain.</p>
<p><strong>Questions to ask:</strong></p>
<ul>
<li>What problem does this portal solve for my constituents? Do they care if I solve it?</li>
<li>What is the best way to deliver our solution to our constituents?</li>
<li>Will my constituents be willing to log in at all?</li>
<li>How will this portal empower themselves or to support our work?</li>
<li>How many people need to use the portal each week, month, or year for it to be successful?</li>
<li>Do we expect that number to grow over time?</li>
<li>Will likely traffic be spread out, or come in a wave?</li>
</ul>
<h3 id="technology-and-data-ecosystem">Technology and Data Ecosystem</h3>
<p>Your portal does not exist is a vacuum. Your organization has lots of technology kicking around these days, and you want the tools you select for this project to build on your overall technology landscape. When I make a recommendation to a client, I spend a lot of time asking them about the tools that are already in place and how they are used. I’m looking for tools that could be the basis of the portal, reveal the expertise of their staff, and would work well with Salesforce (or other technologies I’m part of introducing). I am also trying to understand some of the history about those tools and how they came to be.</p>
<p>If there are other complex systems, and they are working well, that’s a sign that an organization can probably handle more complex architectures. It’s also a sign they probably know how to benefit from that complexity. But it’s also a sign their staff may be consumed with what they have. If they have all simple systems I am going to be inclined to keep my solution equally simple.</p>
<p><strong>Questions to ask:</strong></p>
<ul>
<li>How many interconnected data systems do we have today? Do they work well, or are we struggling to keep up?</li>
<li>If we add a new system will it enhance our digital presence or pull attention from other critical systems?</li>
<li>Are the solutions we are considering related to technology we already use, or are they entirely new?</li>
<li>What kinds of resource, limits, and features does my main CRM provide?</li>
<li>What other technical resources do we already have in place that we could leverage?</li>
<li>Did past projects go well, or leave a bad aftertaste?</li>
</ul>
<h3 id="staffing">Staffing</h3>
<p>Your solution will require staff support. Even the simplest of systems will trigger requests for help using it. The more complex the solution, the more skilled your staff need to be to make sure it runs smoothly – and the more staff you need to support it.</p>
<p>You staff bring two things to the table: time and skills. They need to have the skills to support your solution, and the time to use those skills effectively.</p>
<p>Please do not just plan to push your staff harder, you need to find ways to remove other tasks from their plate or add people to your staff if you want to avoid burn out.</p>
<p>If you use an all-in-one solution, you probably just need people who understand that platform, know how to engage with your user constituents, and can work with to provider’s support team. The more systems you need the more platforms your team needs to understand, and at least one person needs to have a deep understanding of how the whole thing works.</p>
<p>You also should consider aligning your technology selection to what your team either knows, or wants to know. Most of the time you want to play to your team’s strengths not ask them to develop a whole new skill set from scratch.</p>
<p><strong>Questions to ask:</strong></p>
<ul>
<li>What roles do we need to have for your portal to work?</li>
<li>What work do we want done by our staff vs consultants?
<ul>
<li>Who will answer support requests?</li>
<li>Who will respond if there is an outage?</li>
</ul>
</li>
<li>Do our current staff have time for new responsibilities?
<ul>
<li>How can we reduce their workload to offset new requests?</li>
</ul>
</li>
<li>Can we hire more staff?</li>
<li>What technologies do our current technical team members already know?
<ul>
<li>What are they interested in learning?</li>
</ul>
</li>
</ul>
<h2 id="budget">Budget</h2>
<p>Obviously all this will cost money. There will be some combination of license costs, hosting fees, implementation costs, support contracts, staff salaries, and other expenses. Just because a sales person says their solution works “out of the box” does not mean it’s free to get started.</p>
<p>You need to account for the implementation cost: how much will it take to setup and train people on the new system?</p>
<p>You also need to plan for maintenance costs: what will it cost to keep it running every year?</p>
<p>In very broad sweeping terms my experience is that most of the time an all-in-one solution will be cheaper and faster to implement than a two-system solution. But all-in-ones often have higher annual operating costs. Those operating costs are likely to grow linearly with audience size. A two-system solution, while more expensive to setup because of the complexity, will often have a lower annual cost and lower price escalation curve.</p>
<p>When you are considering your options, those are the two budget figures to make sure you understand. What will it cost to setup? How much will it cost over the first 5 years? And what factors will impact those two figures?</p>
<p>Different organization can tolerate different cost structures. If your organization prefers to invest up front and have lower long-term costs, you should make a plan to optimizes for that. Other organizations want the lowest upfront risk, but can tolerate higher costs – particularly if they are linked to a successful and growing solution.</p>
<p>More complex systems are also riskier to build. There is a greater chance of hitting problems that require extra budget to resolve.</p>
<p><strong>Questions to ask:</strong></p>
<ul>
<li>What will each of my main choices cost to implement?</li>
<li>What are the 1, 2, and 5 year costs likely to be?</li>
<li>What factors will drive costs?</li>
<li>What is my organization’s budget tolerance for up-front vs long-term costs?</li>
<li>How firm is the budget? What are my contingency options for overruns?</li>
<li>What are my options to control costs if there are problems?</li>
<li>What can our staff realistically do vs what do we need to pay consultants to do?</li>
<li>If we learn the portal isn’t working, what will it cost to cancel our contracts early?</li>
<li>If my traffic will be in small bursts, will I have to pay extra for those bursts, or pay all year for services we’re not using?</li>
</ul>
<h2 id="closing-thoughts">Closing Thoughts</h2>
<p>There is no one right answer to these questions, and they lead to complex matrix of decision making.</p>
<p>Take the questions I’ve posed, and have a robust and honest discussion about them. Ask your own questions. If you have a consulting partner you trust, have them test your answers and add their questions as well (sure, they are incentivized to get your to build it, but they should still have insights you want).</p>
<p>To make your final selection I suggest you write out a 3-5 year plan – this is a thing consultants can help you do well. Test it with your internal team before taking it to leadership. Make sure you understand it, and that your team understands it, particularly if a consultant wrote it.</p>
<p>I’ve been down this road a few times, and seen organizations make great decisions that led to successful projects, careful decisions that led to portals no one used, and wise decisions to walk away from their vision for a new plan. You need to be open to all possible paths.</p>
<p>Good luck!</p>
]]></content:encoded> </item> <item>
      <title>Architectures for Constituent Portals</title>
      <link>https://spinningcode.org/2024/11/30/architecture-for-portals/</link>
      <pubDate>
        Sat, 30 Nov 2024 12:00:00 +0000
      </pubDate> <guid isPermaLink="false">https://spinningcode.org/2024/11/30/architecture-for-portals/</guid>  <description>One of the common needs, or at least desires, for CRM users is to link their CRM to some kind of constituent portal. There are several ways you can architect a good data pattern for your constituent portal. The trick is picking the right one for your organization.</description> <content:encoded><![CDATA[<p>One of the common needs, or at least desires, for CRM users is to link their CRM to some kind of constituent portal. There are several ways you can architect a good data pattern for your constituent portal. The trick is picking the right one for your organization.</p>
<p>This is the first in a series on what your choices are, and now to select the right one. The architecture you select will drive the implementation cost, maintenance costs, and scalability.</p>
<h2 id="the-purpose-of-your-constituent-portal">The Purpose of Your Constituent Portal</h2>
<p>Before deciding on the architecture of your portal, you need to have a clear understanding of its intended purpose and expected scale. The purpose of the portal will also dictate the direction(s) data flows, and the scale will help you plan the long term costs.</p>
<p>If you can’t clearly state why you need a constituent portal, you don’t need one.</p>
<p>Different organizations have different reasons to create portals. Companies that sell products may want a warranty or support portal. Nonprofits often want a donor portal that provides tax receipts and allowed recurring donors to update their gift information. Member organizations want a place for members to see their benefits, update their information, and renew their memberships. And so on.</p>
<p>The purpose of your portal will determine the direction that data flows through it. There are essentially three directions that data can move.</p>
<h3 id="outbound-data">Outbound Data</h3>






<figure class="figure-left">
  
  <a href="/images/posts/architecture-for-portals/Portal-Data-Outbound.jpeg" target="_blank" rel="noopener noreferrer">
    
    

    











<noscript>
  <img class="rcf-image" src="/images/posts/architecture-for-portals/Portal-Data-Outbound.jpeg" alt="A simple diagram showing data outbound from a CRM to the user." loading="lazy" />
</noscript>

<img class="rcf-image lazyload show-if-js"
     data-srcset="/images/posts/architecture-for-portals/Portal-Data-Outbound_hu_7a33920d67f2e7d3.jpeg 680w, /images/posts/architecture-for-portals/Portal-Data-Outbound.jpeg 709w"
     data-src="/images/posts/architecture-for-portals/Portal-Data-Outbound.jpeg"
     src="data:image/jpeg;base64,/9j/2wCEAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDIBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIADoAgAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5&#43;gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4&#43;Tl5ufo6ery8/T19vf4&#43;fr/2gAMAwEAAhEDEQA/APfaKKoTXbLcNDEAWAySTQBfoqiBeHnzE/KlMd1t4lX8RQBdorNa4ltY90xVgOpFaCMHRWHQjNADqKKKACiiigAooqKS5ggwJJFUn1NAEtFIGDAEEEGloAKKKKACs/yEmu5Sw545rQrOkkeKad0TceOKAJjY5Hyyuv40i2Tr96d2qidZkXaptX3egqUavIf&#43;XV6AJprSMQMSC31NXIuIlx6Vni7eeB8wsn1q&#43;Di3B/2aAH5HrS5HrWbboskZZ5DuJ9al&#43;zw/89T/AN9UAWnmRELMeBRFMkse5TxVTyIScFif&#43;BVGP3F4Ej5RuSvpQBf9&#43;1eU/ESSSTVrcQXyooPI3dK9Wf8A1ZPtXz94mO/Xm8wnb55Bz6ZrpwWtQ4cwname0aNcINChYziXZGCWU9araX4v0vVLxrW3lPmqcFTXAT6pbeHZI1s7pngmj&#43;dAcgcdq57w/q0Gm&#43;IluVVjvf8Ama0&#43;qud2ZTxyp2pn0KOlFRwSebBHIP4lBqSuI9NBVSP/AI&#43;pfwq3VNy8EzPs3K3pQA&#43;5tfPQbDtPqKp2ulPDOJDcOwH8Jqx9tjYchl&#43;opxvYwvXP0FAD7nH2d8elSxjMSg&#43;lUpJ2njKxxNzxyKvIMIoPYUAR/ZYf7go&#43;yw/3BU1FAFSWyR0Oz5W7H0p1vbeQvPzOerVZooAY4zGw9q8l13wjctqILtGyzSnb7Zr1xhlSK5&#43;78MpdziR7mbKtuXB6VrSqezdzmxFL2mh414q0iXRZ4beRlPy54rDs/wDj9hP&#43;2P517lqHgWw1V1e6kkkdeMk1Wj&#43;Gejwyo435U5HNejTx1O2p5lTLqntLwOw0/wD5B1v/ALg/lVmmRRiKJI16KMCn15T3PbWwUUUUhiFFPVRSCNB0UflTqKADA9KKKKACiiigAooooAKKKKACiiigAooooA//2Q=="
     data-sizes="auto"
     width="709" alt="A simple diagram showing data outbound from a CRM to the user."
     loading="lazy" />



  </a></figure>

<p>You can send data from your organization to your constituent. That could be things like receipts, memberships status, or product warranty information. Anything that is data you have, that your member might need to see, but not update.</p>
<h3 id="data-exchange">Data Exchange</h3>






<figure class="figure-right">
  
  <a href="/images/posts/architecture-for-portals/Portal-Data-Bidirectional.jpeg" target="_blank" rel="noopener noreferrer">
    
    

    











<noscript>
  <img class="rcf-image" src="/images/posts/architecture-for-portals/Portal-Data-Bidirectional.jpeg" alt="A simple diagram showing data exchanged between a CRM and the user." loading="lazy" />
</noscript>

<img class="rcf-image lazyload show-if-js"
     data-srcset="/images/posts/architecture-for-portals/Portal-Data-Bidirectional_hu_fa4c796e0edcb26b.jpeg 680w, /images/posts/architecture-for-portals/Portal-Data-Bidirectional.jpeg 702w"
     data-src="/images/posts/architecture-for-portals/Portal-Data-Bidirectional.jpeg"
     src="data:image/jpeg;base64,/9j/2wCEAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDIBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIADsAgAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5&#43;gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4&#43;Tl5ufo6ery8/T19vf4&#43;fr/2gAMAwEAAhEDEQA/APfaKKrXV19nC5GWY4FAFmiqTfapRkOq0my9/wCeiflQBeoqlm6iGTtapLW6&#43;0qezKcEUAWaKKKACiiigAoopCwUZJAFAC0U1JEkGUcMPY06gAooooAKoX0azTQqwyM1fqldnFxAfegBPsbKv7uR1/HNH2WfH/Hy9VZdbERIaCQ4bHSn/wBtp5e7yJPyoAn&#43;xll/eSO344osY1jmmVRgZqKPWFkkRPIkG72qa1Obif60AXCQByeKhiuI5i2xs7eKgunLTJESVU/xf0p32ONR8hK/SgC3njrUMdxHIzBWyV4NR/Yx/wA9n/OoZbdLYCSNsN796AND&#43;Gud8YTSJoU6wzrFIV4JbFdBEcxAn0rzL4nyODGoYgFDwDWlJXqHNiqns6ZZ&#43;GslwEnF1eiXLfKu/OK6nUvE&#43;naXeLaXMuyRhkV5R4at0g0STUkvDDPC/TP3hUHiHV7fWb2C6eQF41w2K7HR9pUOKlivZ0D3W3njuIEljOVYZBqeuV8F6rHqWjRiP/ll8tdTniuCqraHo0qvtFcWqV1/x8wf71XapXud8ThS2084pGpPLCskbKoAY98VmNplzuBWYAZ6VfF7Dj5m2n3p/wBqhx98UASJGqqMgZA61Vtf&#43;Pmf/ep7XsOPkbcfaks9/wA7su3caAJ54UmTaevaqMdnISwlkYj&#43;HBxWlRQBS&#43;wDHDvn/epsFlIsm6Zt4X7tX6KAGjgVwvjnQrnVSksShkRTu5xXdE8Vkarpl1qCbYbtoUIwwA61dJ8jujKrT9pTseOz&#43;Grmz0SS9XIhI5&#43;auR57V70/g&#43;WbTTYyXrtCRgjFY3/CpbEf8vL16dLFU/8Al4eRVwNT/l2P&#43;FIzo0x/269DxxWF4c8NxeHLRoIZC4Y55rezxXm1anPUbR6mGpOnT5GLRiiiszoGGJG6oD&#43;FHkRf3Fp9FADBEi9EA/Cn0UUAFFFFABRRRQAUUUUAFFFFABRRRQB//9k="
     data-sizes="auto"
     width="702" alt="A simple diagram showing data exchanged between a CRM and the user."
     loading="lazy" />



  </a></figure>

<p>You can allow constituents to update information. That could be address changes, support requests, donation schedule updates, event registration details, and more.</p>
<h3 id="data-network">Data Network</h3>






<figure class="figure-left">
  
  <a href="/images/posts/architecture-for-portals/Portal-Data-Network.jpeg" target="_blank" rel="noopener noreferrer">
    
    

    











<noscript>
  <img class="rcf-image" src="/images/posts/architecture-for-portals/Portal-Data-Network.jpeg" alt="A simple diagram showing data exchanged between a system, users, and between users." loading="lazy" />
</noscript>

<img class="rcf-image lazyload show-if-js"
     data-srcset="/images/posts/architecture-for-portals/Portal-Data-Network.jpeg 621w"
     data-src="/images/posts/architecture-for-portals/Portal-Data-Network.jpeg"
     src="data:image/jpeg;base64,/9j/2wCEAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDIBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAHkAgAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5&#43;gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4&#43;Tl5ufo6ery8/T19vf4&#43;fr/2gAMAwEAAhEDEQA/APfaKKKACiiigAooooAKKKKACiiigAppIpM8Vwfjfxfd6BdwxW8QIbliauFNzdkZ1aipw52d/RWR4d1VtY0iG7kTYzDpWtuX1FQXfS4tFFFAwooooAKKKKACiiigAoopM8UALRWbezzCaOOBwCevenNczwqDNHwO4NAFxzsjZvQZrxXxl4ilvLxFWFCFcr8wz3r2iZt1q59VNfPuuhvt7MFJ2zMTj6124JfvDzsxqfu7HofhnxU9vFDZ6lb/AGbK/u3xhTXN&#43;IPEerW/ilo4LlhAXGOeMVS1jxMbzSbWOKPyzDgEsOtcxPqVxczGSUhmPQ11U8Lf94cNfFNe4fSGmzi4sIZdwYlBkg1c7VwvwzvJbrQG81idr4Ga7jPFeXVp8lRo9qjU56amPooqjcXbpOsMZAZhnmoNS9RVIwXEg&#43;afH&#43;7SfZZv&#43;fp6AL1FUvInQZWc/jTYrphciCUgswyCKAH3ty1si7V3Mx6VD5lxdDEaGNe5NPnYLeqZCAqjvSz3qoFEeGZjgUAKkcNohd2y3cmq1xdpcMkKhtxYdRViK0LPvnbeT27CrQgiDhwg3CgAZQYSrdMYNeba94e0y2uIZI7g5mlw/wA3TNemMoZSp6Gsabwzpk0m6SDPOeTWlOp7Myq0vaHlPjnR7TSrW3&#43;ySl9x55zXDYPpX0PP4f0R9sc0KH0B5pP&#43;EO0LH/HlGfwruo4501Y8yrls6lS5gfCoH&#43;w5v&#43;uld/jiqmnabaaXB5NpEI164FXe1cFWpz1Gz06VL2dPkFrPngSa&#43;2suTt61oVSdxHel27LUGon2RkX93K6/jmj7LPj/AI&#43;Xph1e0/ifbj1pU1ezkHyyigBws2df3kzt&#43;OKYLdIbuLavrzQNZss8SdKeZllngZPutmgC4yKw&#43;ZQariwg37wmCPSrVFAABgYooooAKr3kxggLBSx9BVikYAqc0AZ1paxyQtI53s/U&#43;lJazGO6NvgsnZvT2qTTf&#43;Pd/wDfanWQ/duSOd5oAu4ooooAO1UmAa9IPTbV2qRONR5/u0ARXOmWzQk&#43;SGI5x61kyRSRnEdkGX6YrqKTAoAy7PToJIFaS3VG9MVO6CK6gReAM1eqlMQb2Hn1oAu0UyeVYY9xqp9rm8ssISAOeTQBeoqC0uPtEAkIwT2qegAqOeVYYyzMAPepKjuIUnjKNQBR04f6M3&#43;0xNOtbhAWh3DzNx4pouHtAYnQsf4SB1qW2tsv58oHmH9KALtFFFABVa4tVnYNkqw6EVZooApeTdoPlmB&#43;oo2Xv/PRPyq7RQBT8q6I5lA&#43;gpYLNY33szM/qat0UAVL05tm9RyKIbyCVFTcMkVaZQykEZBqqbOIx7AoXHQjtQBE0Elu/mQHKf3KkW/iWMtIdpHVfSohNJaMFlG6M9H9PrU0tvBIhk2qWIzmgCyrB0DA8HmuR8U&#43;NIPDdxHA0bOz810lrLsswz/wg5ryTx3rNjqN9ETblihK5z1row1L2lSxzYmr7Knc9T0jUodZ06K8jHysK0TyK4rwZ4i0&#43;awjskT7PIg4RuM1h6z471HTfEbWSKrQ7wOaX1eftGg&#43;sU4U&#43;c9UoqCznFzaRTcZZQanrBnQnfUKKKKBhRRRQAUUUUAFFFFAFe8thcwGLcVz3FQLYnywryO2OOuKv0UAV3iWOzdV6bTXz9r526gCR8omb&#43;dfQsq742X1GK8s1zwk1vcoz3Kss82FBXpmuvC1fZtnFjaftKehlazr2nrptnLZxr56KAzDgiuZvda&#43;2XX2mWP5&#43;xrb8Z&#43;HzolrADKrbj/CK43vXo0oU3TueNiKlT2lpnu3w91STU9DDS5&#43;RtvNdl2rz74U/wDIDl/66V6D2rx8R/E0Pdw38IWiiiszpCiiigAooooAKKKKACiiigBrLuQj1rmLzwql5KGlu5ztbcvzdDXU0wdaaMa0FLc5TUPA1tqkaJd3EsgXpk1nf8Kq0j&#43;9J&#43;dd96UtP289ifqtPsY&#43;geHrfw/aNb2zEqTnmtiiipOg/9k="
     data-sizes="auto"
     width="621" alt="A simple diagram showing data exchanged between a system, users, and between users."
     loading="lazy" />



  </a></figure>

<p>Your third option is to allow you constituents to exchange information with each other. This could be a any form of member community where they engage with each other.</p>
<p>When planning a network, remember you have to plan for content moderation and acceptable use policies.</p>
<h2 id="primary-portal-data-architectures">Primary Portal Data Architectures</h2>
<p>To support your portal, whatever the data flow is, there are three main data architectures you can choose from, each which a different strengths and weaknesses. The more complex your architecture the more options you will have for what it can support, but it will also increase costs and maintenance efforts. Bigger is not always better – sometimes it’s just more expensive.</p>
<h3 id="1-all-in-one-constituent-portals">1. All-in-One Constituent Portals</h3>
<p>In an all-in-one solution your constituent portal is part of your CRM. For obvious reasons this is the type of portal that your CRM vendor wants to sell you. In Salesforce land this means Experience Cloud. There are also many nonprofit-focused CRMs which include one in their solution. When your portal is part of your CRM you get a bunch of advantages:</p>
<ul>
<li>Data all lives in one place. There is no data sync to worry about setting up or maintaining.</li>
<li>You have one vendor. That means you can centralize your support arrangement and billing.</li>
<li>They are often fastest to implement. They are designed to be fast to market to help make them the obvious choice.</li>
<li>They generally do not require a developer. Again, your CRM vendor really wants you to select this path, so they clear as many hurdles as they can.</li>
<li>There is only one technology stack. By leveraging the technology of your CRM your investments in learning that technology carry over, at least in part, to your portal.</li>
</ul>
<p>There are also downsides:</p>
<ul>
<li>New template system to learn. The CRM’s portal likely has a different template system than the CMS on your main web site. That may be hard to make match your primary online branding.</li>
<li>The vendors make a lot of assumptions. To provide that ease listed above they make assumptions about how data flows, security, user experience, and how other elements should work – those assumptions may not match your ideal solution.</li>
<li>Costs often scale linearly. There are usually license costs that are scaled based on user count meaning your costs grow as the portal grows at more or less a 1:1 rate. While there are price breaks and other incentives this is the right expectation to have for estimating.</li>
</ul>
<h3 id="2-external-constituent-portal">2. External Constituent Portal</h3>
<p>In this pattern an external constituent portal moves the user experience from within your main CRM’s sphere of influence into a separate platform. In my career I’ve mostly built these with Salesforce as the CRM and Drupal as the portal platform – it’s a powerful pairing. The strengths and weaknesses here are more or less the mirror of the all-in-one portal.</p>
<ul>
<li>Your web team may already know the technology. If you use the same technology as your web developers use for your main web site(s) they already know who to handle design and branding.</li>
<li>You have greater control over the User Experience. The assumptions that go into the portal are yours not the ones imposed by the CRM.</li>
<li>You have greater control over security, along with data flow and formats. Since they aren’t built around assumptions you don’t control you have greater control and freedom.</li>
<li>Costs typically grow more slowly. It is less common to have per-user license costs in this context so the cost curve is likely closer to logarithmic.</li>
</ul>
<p>The downsides:</p>
<ul>
<li>You have to handle data synchronization. The two systems means data has to pass back and forth.</li>
<li>You have two different vendors/platforms. When you had one system everything was centralized, now you have two different systems handling constituent data.</li>
<li>This design typically is harder to implement. All those short cuts your CRM provider had in mind for you are gone. Even if you use a purpose built solution it’s going to take more time and effort.</li>
<li>This approach generally requires a developer and/or data architect. To implement this pattern you need someone who understands both platforms – ideally both for setup and support.</li>
</ul>
<h3 id="3-external-constituent-portals-with-data-proxy">3. External Constituent Portals with Data Proxy</h3>
<p>In some situations it makes sense to insert a data proxy, or API layer, between your main CRM and your constituent portal. This creates a layer of abstraction, security, and data augmentation that can benefit a lot of organizations. While this is the most complex of the three main architectures it is one I find is frequently overlooked – in part, I suspect, because no one partner benefits from selling it.</p>
<p><figure>
  <a href="/images/posts/architecture-for-portals/Portal-Data-Architectures-Architectures.jpeg" target="_blank" rel="noopener noreferrer">

    
    
    
    
    

    
    











<noscript>
  <img class="rcf-image" src="/images/posts/architecture-for-portals/Portal-Data-Architectures-Architectures.jpeg" alt="Diagram showing data progressing from the CRM, through an outside data layer and in the process being routed to various cloud resources like an app, portal, or web site." loading="lazy" />
</noscript>

<img class="rcf-image lazyload show-if-js"
     data-srcset="/images/posts/architecture-for-portals/Portal-Data-Architectures-Architectures_hu_6e8b54d43716388f.jpeg 680w, /images/posts/architecture-for-portals/Portal-Data-Architectures-Architectures_hu_1816d098b8620a50.jpeg 850w, /images/posts/architecture-for-portals/Portal-Data-Architectures-Architectures.jpeg 950w"
     data-src="/images/posts/architecture-for-portals/Portal-Data-Architectures-Architectures.jpeg"
     src="data:image/jpeg;base64,/9j/2wCEAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDIBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAEcAgAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5&#43;gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4&#43;Tl5ufo6ery8/T19vf4&#43;fr/2gAMAwEAAhEDEQA/APfaKKKACiiigAooooAKKKKACiiigAoopAwJwCKAAkAEntVdrtRyqM3uBS3WdoGcA8E02UypGvkKrCgBFvOfnRkHqashg65U1UtmmkZhPCFHbvU1tw8n1oAnpMj1qG7Zkt3ZPvYqqsEpQE5OR/eoA0cj1orLlSeNNynbjnrmtDzVWJS3UigCSiq/nr/tflTJLvH3UY/hQBboyPWoJJCbZmTrjiqiwTso3fN77sUAaWR60jMFGScVnNBMqEp8p9d2alm3ssbMNy/xCgCUzFziMZ9&#43;1V3h8kPIZDuJzgGngSycJiNP1pXggj2tI2TnqTQBYQb4BvGcioFjnhyFwV7e1WcgJkdMVX8&#43;RhlY/l&#43;tADg07DoB70sERjLbmyzUnmugy6fL7VNHIjruWgBl1zC1RSQNPGuyUpx2qyRkYNQm1VuhZfoaAIihjtmDuWNPaJmEbL2FKlqoHJLfU1YHAxQBDib0WopGmXjYDmrdFAFRkK2jeuKrpazFg4uTtx92tIgEYPSoJLRWHyll&#43;hoAqPb3CsztcnZ/dqaXPkx9dvfFSpaqB8xLfU1HdMQ6x52r6igBn7r/AKa0yT7Pj59/tnpVhTJGMjEifrTXmgk2q6456EUARDzRb852Z/HFWJN722bcjdjirGAUx2xVUwSIfkk2r6YoAktvO8r9/t3e1Ni4kk9M0BJmHzSAfSpY0Cd8k0ASUUUUAFFFFABRRRQAUUUUAFIyhhyAaWigCuYCh3RsR7dqrNK0yvGY/mBxkCtGkCgHgCgCFpDBbgt97pUJDypiSYK3oDUt1EzqpXqpzVf7NbznzHO1&#43;/NADxEyDCS5PoTmpoJTJuDrhlqJLKGOXzd5z7mnxf62T60AWaKKKACiiigAooooAKKKKACiiigAooooAKgeGFvvKKnqJutADVtIwOcn6mpUiCD5acOgp1AH/9k="
     data-sizes="auto"
     width="950" alt="Diagram showing data progressing from the CRM, through an outside data layer and in the process being routed to various cloud resources like an app, portal, or web site."
     loading="lazy" />


</a>

  
  
</figure></p>
<p>This setup certainly isn’t for everyone, but when it’s the right fit it makes a huge difference. It also creates a significant number of iterations – all of those arrows could be one directional or two directional.</p>
<p>An extra data layer makes the most sense when data from your CRM is going to multiple places and/or needs to be augmented by an external data source. For example think about an organization that provides trail maps for hiking or paddling. All that data geocoding those trails does not need to be in your CRM, but you probably want to know which members are interested in which trails so some trail data is needed there. Meanwhile in public you want a great deal more information all linked together. An organization like that may have a portal for members, a web site to find trails for non-members, apps for iPhone and Android, and perhaps a public API for other people to use to build their own web sites with. That’s a lot of API calls all with different data sets, all of which need to be very fast.</p>
<p>Your CRM is designed to be very stable and reliable – it is not designed to be very fast. What’s more, you often have limits on the number of API calls you get in your package (with extra fees charged if you go over). By inserting another layer between your CRM and other services you can bypass these limitations.</p>
<p>There is another added benefit to this design pattern, and that is increased security. By creating an additional layer in your system you are able to create a separation of concerns between the various systems. Each should have access to only the data is absolutely needs from any of the others.</p>
<h2 id="how-to-pick">How to Pick?</h2>
<p>There are lots of considerations that go into selecting the right architecture for your project – which is why that’s getting it’s own post soon.</p>
]]></content:encoded> </item> <item>
      <title>Salesforce Github Actions</title>
      <link>https://spinningcode.org/2024/01/salesforce-github-actions/</link>
      <pubDate>
        Fri, 05 Jan 2024 23:07:12 +0000
      </pubDate> <guid
        isPermaLink="false">https://spinningcode.org/?p=2120</guid>  <description>&lt;p&gt;&lt;em&gt;I started this post back in November, but got &lt;a href=&#34;https://spinningcode.org/2023/12/goodbye-mom/&#34;&gt;rather distracted last month&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I have been wanting to setup a CI pipeline for Salesforce scratch orgs for a long time. The documentation out there isn&amp;rsquo;t great, and it&amp;rsquo;s taken me awhile to get around to powering through the details. For testing that &lt;a href=&#34;https://spinningcode.org/2023/11/salesforce-nonprofit-and-education-scratch-orgs/&#34;&gt;my scratch org configurations for Education Cloud and Nonprofit Cloud&lt;/a&gt; to work properly I finally bit the bullet and setup a Github action.&lt;/p&gt;</description> <content:encoded><![CDATA[<p><em>I started this post back in November, but got <a href="/2023/12/goodbye-mom/">rather distracted last month</a>.</em></p>
<p>I have been wanting to setup a CI pipeline for Salesforce scratch orgs for a long time. The documentation out there isn&rsquo;t great, and it&rsquo;s taken me awhile to get around to powering through the details. For testing that <a href="/2023/11/salesforce-nonprofit-and-education-scratch-orgs/">my scratch org configurations for Education Cloud and Nonprofit Cloud</a> to work properly I finally bit the bullet and setup a Github action.</p>
<h2 id="what-youll-need">What you&rsquo;ll need</h2>
<ol>
<li>An SFDX-style project on Github that you want to setup for automated testing that includes a <a href="https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_scratch_orgs.htm">Salesforce scratch org</a>.</li>
<li>An org with Devhub enabled. I recommend using a production org for this, not a developer org because of the <a href="https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_scratch_orgs_editions_and_allocations.htm">higher scratch org limits.</a> There are <a href="https://www.linkedin.com/posts/jasonlantz_enable-dev-hub-features-in-your-org-activity-7118686134511030272-JOOa?utm_source=share&amp;utm_medium=member_desktop">no good reasons not to enable Devhub</a>.</li>
<li>A working install of <a href="https://developer.salesforce.com/tools/salesforcecli">sf cli</a> with a connection to that devhub-enabled org.</li>
</ol>
<h2 id="setup-github-secret">Setup Github Secret</h2>
<p>For your action to work it will need to use your connection to your Salesforce Devhub. Salesforce CLI stores the needed key on your local machine, which includes tokens to open the org. You will need to put those keys into a <a href="https://docs.github.com/en/rest/actions/secrets">Github secret</a> for proper security.</p>
<p><em>Note: Should that key ever be compromised, they attacker will be able to open the org as the same user. I suggest you consider using a dedicated user with either a <a href="https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/dev_hub_license.htm">Free Limited Access License</a>, or another similarly restrictive set of permissions. These users are designed to avoid exposing data if they are compromised (they are also free, which has some upsides). That said, you can get yourself started with any user that can access to Devhub objects and then switch out to something more restricted later.</em></p>
<p>To get the key from sf cli, run <code>sf org display -o [your devhub alias] --verbose</code> where <code>[your devhub alias]</code> is the name of the connection you selected when setting up the connection. The output of that command will include: &ldquo;Sfdx Auth Url&rdquo;, that is the value you need in your Github secret.</p>
<ol>
<li>In Github, go to your repository&rsquo;s settings, expand Secrets and Variables from the left menu, and select Actions.</li>
<li>Create a new Repository secret.</li>
<li>Name the secret <code>DEVHUB_SFDX_URL</code></li>
<li>Copy and Paste the Sfdx Auth Url value into the Secret field.</li>
</ol>
<h2 id="create-the-github-action">Create the Github Action</h2>
<p>The full details of <a href="https://docs.github.com/en/actions/using-workflows">Github Actions</a> are a deep topic, as are the full capabilities of sf cli for testing. So I&rsquo;m going to focus on setup of an action the creates your scratch org. From there you can expand your workflow as you need.</p>
<p>In your project root create a <code>.github</code> folder, and <code>workflows</code> folder within if you don&rsquo;t already have them. In the workflows folder create a file called <code>buildScratch.yml</code>.</p>
<p>Copy the following code (explained below) into your file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yml" data-lang="yml"><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">test run scratch</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Definition when the workflow should run</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># The workflow will run whenever an event happens on a pull request</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pull_request</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">types</span>: [<span style="color:#ae81ff">opened, synchronize]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">validate_scratch_deploy</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Install Salesforce CLI</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;Install Salesforce CLI&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          npm install @salesforce/cli --location=global
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          nodeInstallPath=$(npm config get prefix)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          echo &#34;$nodeInstallPath/bin&#34; &gt;&gt; $GITHUB_PATH
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          sf --version</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Checkout the code in the pull request</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;Checkout source code&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v3</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Load secret for dev hub</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;Populate auth file with SFDX_URL secret&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">shell</span>: <span style="color:#ae81ff">bash</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: <span style="color:#e6db74">&#34;echo ${{ secrets.DEVHUB_SFDX_URL}} &gt; ./SFDX_URL_STORE.txt&#34;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;Authenticate with dev hub&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: <span style="color:#ae81ff">sf org login sfdx-url -f ./SFDX_URL_STORE.txt -a devhub -d</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Create a scratch org</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;Create scratch org&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: <span style="color:#e6db74">&#34;sf org create scratch -d -f config/project-scratch.json -a our-scratch-org --duration-days 1&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Deploy Project Manifest</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;Deploy Metadata&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: <span style="color:#e6db74">&#34;sf project deploy start --manifest manifest.xml --target-org our-scratch-org&#34;</span>
</span></span></code></pre></div><h3 id="action-breakdown">Action Breakdown</h3>
<p>The first few lines are common to all actions, it gets a name, then a set of conditions to trigger the workflow. In this case we&rsquo;re calling our workflow &ldquo;test run scratch&rdquo; and it will run when a Pull Request is opened or gets new commits.</p>
<p>The jobs section is the more interesting bit. We tell Github to create a virtual machine using the most recent version of Ubuntu Linux. Next we install Salesforce CLI onto that virtual machine. Once that&rsquo;s done, we tell it to checkout our code from Github onto this virtual machine. With all our tools in place we&rsquo;re ready to start the real work.</p>
<p>The next two steps extract the Github secret we defined earlier into a file on the virtual machine and use that file to authenticate to your Salesforce devhub. All that work gets this temporary virtual machine into the same position as your personal device probably was when we started.</p>
<p>The last two steps create the scratch org based on a hypothetical scratch org configuration file and gives the scratch org a short life of one day. Finally deploy your project&rsquo;s manifest-tracked metadata. This last step could use any version of the <a href="https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference_project_commands_unified.htm#cli_reference_project_deploy_start_unified">deploy command</a>. It could also be added by more steps to deploy other metadata, run tests, or anything else you want to have happen.</p>
<h2 id="closing-thoughts">Closing Thoughts</h2>
<p>Building from this point you can do all kinds of things. For example, if you don&rsquo;t need to gain access to the org itself, you can include a step to delete the scratch org when you&rsquo;re done to help avoid the active scratch limit.</p>
<p>You&rsquo;ll want to pay attention to the limits of your org to see if you need to tweak the triggers of your workflow. If you aren&rsquo;t using a production org you scratch limit is only 6 per day, and three at a time; I blew through that testing the steps in this article. Even larger orgs only have 200 per day. So be thoughtful about when your action runs, and how often you clean up.</p>
<p>As always, if you find a flaw in my solution please let me know. I&rsquo;m always interested in making these better.</p>
]]></content:encoded> </item> <item>
      <title>A Salesforce Data Migration Pattern</title>
      <link>https://spinningcode.org/2023/10/a-salesforce-data-migration-pattern/</link>
      <pubDate>
        Mon, 23 Oct 2023 01:55:06 +0000
      </pubDate> <guid
        isPermaLink="false">https://spinningcode.org/?p=2097</guid>  <description>Over the last few years I’ve done a number of large data migrations into Salesforce, and developed a pattern I like to follow.</description> <content:encoded><![CDATA[<p>Loading large amounts of data into Salesforce is a non-trivial exercise. While traditional databases can often be loaded in nearly any order, or with just a few simple considerations for foreign keys, Salesforce’s platform behaviors require several special considerations.</p>
<p>Over the last few years I’ve done a number of large data migrations into Salesforce, and developed a pattern I like to follow. This pattern allows me to load data efficiently at any scale. While the implementation details will vary, you can adapt this pattern to your projects.</p>
<p>Efficiency matters more the larger your project: for a small project, this is overkill. If you are loading 1,000 Contacts it will probably take you longer to setup my process than just format the file in Excel and load it through Data Loader. But if you need to load 100’s of thousands of records, millions of records, across lots of different objects, this pattern can save hours or even days.</p>
<h2 id="migration-process-overview">Migration Process Overview</h2>
<p>The general concept here, is that you’ll run your migration in two major phases:</p>
<ol>
<li><strong>Prepare the data in a staging database.</strong></li>
<li><strong>Load the data into Salesforce.</strong></li>
</ol>
<p><figure>
  <a href="https://lh4.googleusercontent.com/XAowaCgtZ8KVRhd2apma0_S-_Ru0XGSpi_q-6cehO0hNV7Nl9GtaaSl6K47ti4gvbfMWZqBZ9fPmBwnQh-4Bo_bBZOQcKxji2j0JlyMu2UZOQPTMQE1TE48TfMM7XTQ9xVVdL3i3yxdzLs6yPldEopM" target="_blank" rel="noopener noreferrer">

    
    
    
    
    

    <img class="rcf-image external-image" src="https://lh4.googleusercontent.com/XAowaCgtZ8KVRhd2apma0_S-_Ru0XGSpi_q-6cehO0hNV7Nl9GtaaSl6K47ti4gvbfMWZqBZ9fPmBwnQh-4Bo_bBZOQcKxji2j0JlyMu2UZOQPTMQE1TE48TfMM7XTQ9xVVdL3i3yxdzLs6yPldEopM"alt="Diagram of a two stage migration, first from the source data into a Salesforce staging database, and then from the staging database into Salesforce."  loading="lazy" />

    </a>

  
  
</figure></p>
<h2 id="salesforce-schema-mirror-staging-database">Salesforce Schema Mirror Staging Database</h2>
<p>The key to this process is that staging database in the middle.</p>
<p>In my experience having a database that is a clone of Salesforce’s schema allows you to fully prepare the data prior to loading. It also gives you a source of truth when handling partially loaded data.</p>
<p>Salesforce is slow to load compared to most traditional databases. By having a staging database you can load fast, gives you a chance to insert steps into your process that are hard in other contexts. These steps allow for testing, speed-enhancements, and error recovery.</p>
<p>Some ETL tools make a staging database easy to build, others do not. If you aren’t sure how to build such a database (or it seems like a huge effort to re-create all those tables), you can use <a href="/2022/05/getting-started-with-salesforce2sql/">Salesforce2Sql</a> – that’s why I created it. It will clone your Salesforce org&rsquo;s schema into any of its supported databases.</p>
<h3 id="testing-and-error-recovery">Testing and Error Recovery</h3>
<p>The staging database lets you test for errors after you do your initial conversion; before you load it into Salesforce. You can leverage reporting and scripting engines designed for that database. You can log and error trap during your loading process far more gracefully than the Salesforce APIs support by default.</p>
<p>I often add one more table than just the objects: a logging table. This allows me a place to write the rows from Salesforce error files, and also log the time it takes for each process to run. I can see exactly what errors my process encounters at the record level during testing, and measure the running time.</p>
<p>This database will also give you a place to trace what has, and has not, been loaded into Salesforce. More about how to implement this and drive performance to come.</p>
<h2 id="transform-the-data">Transform the Data</h2>
<p>Using the tool of your choice, create a process to transform the data from the source data into your staging database. How you do this stage could be a series of posts by itself. For my ideas on a good process for this I suggest <a href="/2023/06/queries-on-data/">my Queries on Queries talk</a>.</p>
<p>Your process will have a mountain of small details – I often describe it as &ldquo;hard <em>and</em> boring&rdquo;. Done well this is your best point in the process for testing your work. Test thoroughly! You should run this process so many times you lose count.</p>
<h3 id="salesforce-migration-keys">Salesforce Migration Keys</h3>
<p>One important detail is that you will want to leave the main record <code>Id</code> field null. Your legacy Id goes into a legacy Id field, but the main Id field should be empty. We’ll use that in the loading stage to determine which records were successfully loaded and which need follow up attention.</p>
<p>Every object you are migrating should have a legacy Id field that links back to your source database. These should generally be text fields, set to be external Ids and unique. These fields will both help with the migration itself, but also the validation process – and should you need to, you will be able to update the data post-migration using those same keys.</p>
<p>To handle references between records use the legacy Ids as the lookup Id values. For example, on a Salesforce Contact there is an <code>AccountId</code> field to reference the parent account. The Account’s legacy Id should be in <code>AccountId</code>. Often this value is already in your foreign key fields so it can be a real time saver in your transformation build. We’ll see in a minute how we use those to resolve to new Salesforce Ids as we load data.</p>
<h3 id="data-cleaning">Data Cleaning</h3>
<p>This is also the time and place to do whatever data cleansing you plan to do in your process. You can do that work post-launch as well (mostly). I highly recommend this cleansing be automated for large data sets. If you can&rsquo;t automate it, do it pre-migration in your old system, or post-migration in Salesforce.</p>
<h3 id="pre-load-data-validation">Pre-Load Data Validation</h3>
<p>Using the staging database your transformed data can be fully validated before you load it.</p>
<ul>
<li>Check your references: Make sure all your lookup fields are populated with valid data.</li>
<li>Check your record counts: Do you have the expected number of records in every table?</li>
<li>Check your critical fields: All data points are created equal, but some are more equal than others. Check those a few extra times.</li>
</ul>
<p>If you have the time and resources, you can write scripts and other automations to run these tests for you. The more the better.</p>
<h2 id="loading-salesforce">Loading Salesforce</h2>
<p>Finally, all that data you just transform and staged is ready for high volume loading. For each object you run two steps:</p>
<ol>
<li>Insert the data via the Bulk API ( <em>Insert not Upsert!</em>), record the start and end time and all errors in your log table.</li>
<li>Update the records in your staging database to add the new Salesforce Id into the source record&rsquo;s Id column (the one I told you to leave blank before).</li>
</ol>
<p>When there are no null values left in the Id column, you have loaded all your data. If there are records that refuse to load, for any reason, you will know because the Id will be null. If you logged the errors you can see why.</p>
<p>You will also use those Ids in later jobs to update the reference Ids. Remember, we put the legacy Id into your reference fields, when you actually load the data you need to replace that legacy Id with the actual Salesforce Id.</p>
<p>When possible you should build these load jobs to only load records without a Salesforce Id already assigned. That will allow you to safely re-run the job if it encounters errors that lead to partial success (like record locking, see below).</p>
<h3 id="why-not-upsert">Why Not Upsert?</h3>
<p>People used to loading small amounts of data will be tempted to use Salesforce&rsquo;s upsert command. The benefit is that it allows you to use those legacy Id values directly instead of swapping for the newly generated Id. But as record volumes grow, upsert performance drops – I’ve had projects where I measured it at ⅓ the speed of insert, I heard of projects where it got far worse than that. The larger the dataset, the more important it is to use Insert.</p>
<h2 id="playing-nice-with-salesforce">Playing Nice with Salesforce</h2>
<p>To make sure your data loads correctly, and efficiently, there are three more important details you still need to plan for:</p>
<ol>
<li>Automations and Sharing Rules</li>
<li>Object Load Order</li>
<li>Record Load Order</li>
</ol>
<h3 id="automations-and-sharing-rules">Automations and Sharing Rules</h3>
<p>Automations take time to run, even small amounts of time add-up when loading large amounts of data. To the degree possible, you want automations off. Some automations you want to replicate in your transformation process – particularly if it’s a simple field value or record creation. Some automations you want to defer and run later, like custom roll up values via <a href="https://github.com/SFDO-Community/declarative-lookup-rollup-summaries">DLRS</a>, NPSP rollups, or similar approaches. And some automations you cannot disable at all.</p>
<p>Sharing calculations in Salesforce are really a special-purpose automation. Just not one you often think about unless you’re doing manual sharing. Like all automations in Salesforce, the more data you load, the larger the impact of these calculations. Salesforce allows you to <a href="https://help.salesforce.com/s/articleView?id=sf.security_sharing_defer_sharing_calculations.htm&amp;type=5">defer these calculations and run them in the future</a>. The more complex your security setup, the more impact this will have (open security models can generally ignore this consideration).</p>
<p>The person doing the data loading needs to work with the folks that implemented those automations to map out which can be disabled, which can be deferred, and which must to be tolerated.</p>
<h3 id="object-load-order">Object Load Order</h3>
<p>In Salesforce, object load order is critical. You cannot disable or defer assignment of required references. So you need to understand the object hierarchy and relationships.</p>
<p>Generally you start with objects that have no dependencies: e.g. Account, Campaign, Product, Lead.</p>
<p>Then proceed to objects that have relationships to those: e.g. Contact</p>
<p>Then to objects that can have relationships to objects from that previous layer: e.g. Opportunity, Account Contact Relation, Campaign Member</p>
<p>When possible, test running two objects in parallel. What exact combination is most efficient will vary by org details and data volumes. My experience is that you will be able to run objects in 4-5 groups usually with two or three objects loading in parallel.</p>
<p>Ideally we’d just load records and not have to go back and update, but if there are circular references, or record hierarchies you’ll need to update records after insert. Plan that second pass into your sequence.</p>
<h4 id="users">Users</h4>
<p>Salesforce Users are a special case. If you have a security model where record ownership is important, you need to load Users first.  If you have an open security model, I recommend loading Users last – and the smallest number of Users possible.  Remember, Salesforce bans User deletion, so you must be as careful as possible about loading them.  I <em>never</em> like to load Experience Cloud Users if I can avoid it – 1,000’s of accounts that will never be used but cannot be deleted is sub-optimal.</p>
<h3 id="record-load-order-and-record-locks">Record Load Order and Record Locks</h3>
<p>Salesforce has aggressive record locking to deal with concurrent edits and updates across relationships. Great for day-to-day operation; frustrating when you&rsquo;re loading data.</p>
<p>The first place people often encounter this is when they go to load Opportunities. Opportunity bulk load can run into massive problems with Account records being locked because another Opportunity is being loading for the same Account in a parallel process. If you <a href="https://developer.salesforce.com/blogs/2014/02/the-salesforce-bulk-api-maximizing-parallelism-and-throughput-performance-when-integrating-or-loading-large-data-volumes">sort the records by the locking parent record</a> you can often reduce, if not eliminate, your record locking issues.</p>
<ul>
<li>Sort Contacts and Opportunities by Account.</li>
<li>Sort Campaign Member by Campaign.</li>
<li>Some Objects benefit from sorting by multiple fields: e.g. <a href="https://developer.salesforce.com/blogs/engineering/2014/08/managing-task-locks-data-loads">Tasks which you want to sort by WhoId and WhatId</a>.</li>
</ul>
<p><strong>Use Serial Mode only as a last resort.</strong> Serial mode is ⅕ the speed of Parallel mode most of the time. There <em>are</em> situations that call for it. But it should never be your default go-to solution. Try everything else first before resorting to serial mode. Since you have tracking of which records were loaded or failed, if you design your load job carefully you can just re-run to resolve small numbers of record locks.</p>
<h3 id="extra-sorting-trick">Extra Sorting Trick:</h3>
<p>It turns out, in many cases the way data gets entered over time will gather it in useful patterns. So sorting data by a date field can radically reduce record lock contention. If you cannot figure out what field to sort by (often because sorting by field 1 causes locking issues on another object) try sorting by a date field and see if that helps.</p>
<p><em>Warning: depending on your data patterns, it can make the problem vastly worse too.</em></p>
<h2 id="mock-runs">Mock Runs</h2>
<p>A mock run is a test load into a sandbox that should involve you going through all the steps to load the data – starting with extracting it from the source system.</p>
<p>I personally recommend at least two full test mocks of your process.</p>
<p>If you’re working on a tight budget that may not be feasible (migrations are the first place project leaders trim budgets, and the first place users complain about errors), but that doesn’t mean multiple tests aren’t valuable.</p>
<p>The first test will go poorly, but you’ll learn a lot. The second test will, hopefully, go far better, but you will still learn a great deal.</p>
<p>In your testing you should expect to find places where your mappings are wrong, your transformations are incorrect, your testing is inadequate, your load order doesn’t work, you have source data patterns not accounted for, and more. Make time for good testing, you’ll thank yourself later.</p>
<h2 id="final-considerations">Final Considerations</h2>
<p>Large volume data loading in Salesforce is a deep topic. For all this is a long article, I’ve left out a lot of details. I designed this pattern to support high speed loads, rigorous testing, and error recovery. But within each of step of this pattern I could write articles this long or longer. You should continue to research the topic and adapt your implementation to your project.</p>
<p>A few sample topics you might consider:</p>
<ul>
<li><a href="https://help.salesforce.com/s/articleView?id=000385636&amp;type=1">How to set audit fields.</a></li>
<li>How to <a href="https://help.salesforce.com/s/articleView?id=000385540&amp;type=1">enable and disable triggers in production</a>.</li>
<li>Wipe and reload an org&rsquo;s data.</li>
</ul>
<p>You may even need to do something I’ve never encountered before.</p>
<p>But in any large volume Salesforce data load, the general pattern outlined here will serve you well.</p>
]]></content:encoded> </item> <item>
      <title>Thoughts on My First Dreamforce</title>
      <link>https://spinningcode.org/2023/09/thoughts-on-my-first-dreamforce/</link>
      <pubDate>
        Sat, 30 Sep 2023 23:29:52 +0000
      </pubDate> <guid
        isPermaLink="false">https://spinningcode.org/?p=2086</guid>  <description>&lt;p&gt;I&amp;rsquo;ve been full time in the Salesforce eco-system for a little over five years. I have eight certifications, co-lead a community open source project, have been on the planning committee for &lt;a href=&#34;https://nonprofitdreamin.org&#34;&gt;Nonprofit Dreaming&lt;/a&gt; twice, and am an MVP. But until this year, I&amp;rsquo;d never been in &lt;a href=&#34;https://www.salesforce.com/dreamforce/&#34;&gt;Dreamforce&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;d like a breakdown of the content from Dreamforce, there are many better sources for that. Salesforce+, the plethora of blog posts written more quickly by people who went to more sessions. This is just my reflections on my experience.&lt;/p&gt;</description> <content:encoded><![CDATA[<p>I&rsquo;ve been full time in the Salesforce eco-system for a little over five years. I have eight certifications, co-lead a community open source project, have been on the planning committee for <a href="https://nonprofitdreamin.org">Nonprofit Dreaming</a> twice, and am an MVP. But until this year, I&rsquo;d never been in <a href="https://www.salesforce.com/dreamforce/">Dreamforce</a>.</p>
<p>If you&rsquo;d like a breakdown of the content from Dreamforce, there are many better sources for that. Salesforce+, the plethora of blog posts written more quickly by people who went to more sessions. This is just my reflections on my experience.</p>
<p><a href="/wp-content/uploads/2023/09/IMG_3803.jpeg"><figure>
  <a href="/wp-content/uploads/2023/09/IMG_3803-1024x768.jpeg" target="_blank" rel="noopener noreferrer">

    
    
    
    
    

    
    











<noscript>
  <img class="rcf-image" src="/wp-content/uploads/2023/09/IMG_3803-1024x768.jpeg" alt="Overhead picture of people milling around outside at Dreamforce" loading="lazy" />
</noscript>

<img class="rcf-image lazyload show-if-js"
     data-srcset="/wp-content/uploads/2023/09/IMG_3803-1024x768_hu_264d8bf8d0380c0a.jpeg 680w, /wp-content/uploads/2023/09/IMG_3803-1024x768_hu_f6612e902ea53048.jpeg 850w, /wp-content/uploads/2023/09/IMG_3803-1024x768_hu_c7763f0f2b7d4cf5.jpeg 1020w, /wp-content/uploads/2023/09/IMG_3803-1024x768.jpeg 1024w"
     data-src="/wp-content/uploads/2023/09/IMG_3803-1024x768.jpeg"
     src="data:image/jpeg;base64,/9j/2wCEAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDIBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAGAAgAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5&#43;gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4&#43;Tl5ufo6ery8/T19vf4&#43;fr/2gAMAwEAAhEDEQA/AMzQ5pnXMsxI9Kt35jm2YmKBfbrUsjItpvgCB1UfLVZWuSVyse1hR7UzLWnWpWf7Q0wKbcHjrWjf2El3MJoE3IUx&#43;NQWd4sVm8dwF3fw8VFcatdadNaCN8wytg47Vp7UDpdOtJVRUlhZcxYPFRanciy0wBWYMDinNrdy0s1uAcQqGDeuazb26W5hYujeXnnFOnVCoc9p9tI9xcXf8LNVv7S4O5Hbr/CMmoV1yytrd7dM/MT2qOJt0YkVWIz1DYrM0NFbiQtJmR&#43;o6rVqeWTaP7uKg&#43;wjmQSSfMM9asRukwQH&#43;GumlVMqosbkR9DSRlxJvJ3J/dqzKwAXjis2e6SOXANdP1r957M5vZVP4hekusEbPlqJ9RkIYJhaqOwkGd9EcYfnfWVXDVKlS5rTxNOnTLdvfFI3SQbs/wAVJLdgjZt7VWniwnDdarP5sf3sHdXN7Koqhpek6ZRayuozuZXX2IpyJOG5PAr0jUtUs7/S7iWK0jEi8ciuEstFvL28Zyw8ktnCmuH2R0&#43;z9mVBGZVd2fO0VNDZNOImXLKrZAJrTuLNbR2RbdlQdSe9XUtVFh50YVcDpmun6t7On7Q5va&#43;0qezIHvI4bllbhpE201Yp4LCR5UO0HIJHBFXLbwlNqdk16bkRrjK5rltZvrxLVrRLosg&#43;Xk1n7Q1CfTLa6QziaJHPOyqUeqIkX2R0BYNjNYyI8UWZXOf726mB5lcTjmIsAxHeulAdY&#43;uPZW4jV1LE/LkVBD4iuYyxMCyKD6YrKnC3dzDJCQwT71aov7WK3ZZowv8AtVov4hn/AMuy5J4y0&#43;ORI7qJk3DtzUqHSb91kh1BUJ/hfiuA8RRNJMksCHy&#43;7VivfSRuuH&#43;7TqbiPZTokjIvkXEcv&#43;61S2Wm3EMmyaAlPWvH7bWL0yfuLqRW9mxW9B4v8Q2Cj9&#43;ZF/2uaLsy9kemXNgiDOCBVWCBJJcMPlFcrbfFCdFCX1mHHqBit7TfH&#43;g3kiRuhidvair7SpTHTpUzZEbC8ZJpRFFcHLRL61sQxw2ceyBQvvXnWky6nqEm8OxUnIZhXUT3NxCArMPmA2/XvXLVOk07p3YOigOfeuejhvdT1JdP85YkPLEVenvZrawll&#43;/KeEK1z0K6gty1zhgW96PZ1P8Al2Z1KlOn8Z2j6I2n2rAawxSNT&#43;7zwa4rW7aNNPMq9d3WmPqc7SsjyklT93NO1af7TpGzHz56CuGp7T2poca1yXcpwRnvTFuWhXyllUhjyBU39nmWBkL7CG/SmPBaWsm3ALKO9ekjMtWV4&#43;JEQDimIzXE&#43;LiNvKXrzUEFxGJD8oGegJ5q2LkNCvloWw3NMCpq2oSyWRt4Y/kFcoYJZThQa7G4tXGZoACx5MY5qnNGgdJY12sfvLitQOejt5Ij8p5rQje5SPkk1a8pDJ8yYY96fsmRtmVw3FAFQ3f2hDG6KAv8VMt7Ly5VljlG5eamlsJYd74DJ6023jD/AHXw/wDdoEerWutLFCI/s5iCH0qrqGpm8nDqQFXpj0pEgubrdA5AIGA1Ub6yOn2chD73z2rgqs1NTN3dRokThU&#43;lSHSb5lybonH4VmWOpziwR4cbh1yKju9R1O5IYv5UX3eO9d&#43;Gq4f2X7w4atLEVKvuBLYtDdrIFDP/ABHtUl199d42s&#43;BntUNzdXUVzFHA2dq8gjrVlrz7VZP9stTEV4V8cGuF7npey0ORvpbhdTKAAh84ANY7x3Lq0m7J3Y&#43;ldMNGJlZjMA5U7d39Kxfss9is4L7nJxt9q6aVQzJ7SETwB2KNNyKfDBcaaTK5zE33lqLT7X7LGZJpPmPK1aWSSQvHu3g81qZl/RdTtcyuV3SbTtzVSS3lvp9yoEdcsc96x5JLqNmCJ5caH5h61sW1/G8SyB/LdV4zzmgClNJEZNsishHHAqJWeLL53oTW4iW19agTjYc8N3Oaz762NiwEbrJGf0rIRNNqVjsFs43EqDuH8qqraK&#43;6RI1x2qifIJy/WtFbm2itNh3pLt/OtaVUzPSLwRSQkkhADvUqeprmPtj6h9oWPt8vNacsT3Mtu8ecFDkVxhvpNK1N45uAJDXj/wAQ6Tc0uRIy9rJ9&#43;tIJBJH5TSnahzXM3OrKJ45oIwQQctUuk6jK8recAFdq02A2I7pLi/8AIQbd3CsRV/XdLu10hVaTG35sCpdOsrS7mKFgpHzAj2qTxHrMVlao5fzQDt2460VKlSp/DNLnA3F/esiBFJ2fLuxzVGaVwshyzyN94&#43;las2uxRXW&#43;3hXY4&#43;ZWHes&#43;3uZTcSEhD5mSFrqpGZILOUWqCeRd0h&#43;XmrikRIsUWFkC/e9a57VJ7mWZHBwE4xWjay3E1mHChnUfM3oK6jMr7bq/ll3vjLYons5bUpuPyimWV75TszctmrqTfaXdJ2GONprKoBZt5PtkBLtsKrhfeuj8OeHo7u0klnKuJPlX5vu1x4nPlSQKMjdxirenXF3bxsqTvGg6rnFIB&#43;s6XFpd68WQ&#43;08YqpMWu40bb823AxTZpWmuXeRyd3941PJG1vHFNC4p0hHoEd9E6ny9u5Pu1w/iTZfaiCI8N0Zq6bSo0uY3khX5qkltbd0dZIlyfvGvD9r7M1M22063i0ZoYwrttyPc1Homm3tyVLwIkYPLGug0T7HbsWjaJ0HZjzWpcz2CwGUsYVbpjvXR7XQ19kc5Jc3ej61/o8IniZcHaKnu9OF9YbpoHzyyg1lal4ktrG52RyB9vVhW14c8WpfAw3AUr/CRTpe09mFOnTOD1DTHtB86feGRWZ80MsTgMStdlqok17UrlNOiPlW4yxrMW0ezlUShPmXKlq7qRzMyZIzcWrnZyzVe0iREs5rUxMZW9PSo/tHmyvEqr1521c02C5sLs3IhLqRtpgYhMMVwQI2K7sNU19CIpIWWQfOPujtT9QaM3QjQ7AzFm&#43;tRK0UAjneTfJuxjHStAJZ0aBfNiU7F5LAVFFdm4DIhZmbmtWx1SFJZIblQ8Ui8riqAvIUkBtoQqKSPetQGywyIqnG5sdPWmMbiG2WSTlX7elTtqgViI0JcrwPSoIEluZF899iehoA//9k="
     data-sizes="auto"
     width="1024" alt="Overhead picture of people milling around outside at Dreamforce"
     loading="lazy" />


</a>

  
  
</figure></a> It felt a lot more crowded than that, particularly for my first work event post pandemic.</p>
<p>For my first post-pandemic work trip it was a more than a little overwhelming. Dreamforce was supposed to have roughly 40,000 people. The largest professional in-person event I&rsquo;ve been part of in 4 years was my wife&rsquo;s department gathering at our house – I think 10-15 people came. This was a little bigger.</p>
<h2 id="networking-is-still-most-important">Networking is Still Most Important</h2>
<p>For me the most important part of any professional conference is the networking. And you put 40,000 people together, there are going to be interesting people to meet.</p>
<p><a href="/wp-content/uploads/2023/09/IMG_4030-scaled.jpg"><figure>
  <a href="/wp-content/uploads/2023/09/IMG_4030-300x225.jpg" target="_blank" rel="noopener noreferrer">

    
    
    
    
    

    
    











<noscript>
  <img class="rcf-image" src="/wp-content/uploads/2023/09/IMG_4030-300x225.jpg" alt="Friends from the Knitforce group at the Amplify breakfast." loading="lazy" />
</noscript>

<img class="rcf-image lazyload show-if-js"
     data-srcset="/wp-content/uploads/2023/09/IMG_4030-300x225.jpg 300w"
     data-src="/wp-content/uploads/2023/09/IMG_4030-300x225.jpg"
     src="data:image/jpeg;base64,/9j/2wCEAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDIBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAGAAgAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5&#43;gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4&#43;Tl5ufo6ery8/T19vf4&#43;fr/2gAMAwEAAhEDEQA/AN/4fAvobRn&#43;FzXXlBjqM1ztn51lEY4IkiRj/DTJnvBJ/r8Vw&#43;yOo17i1jJ3u3zYqnZQlwybtq5rNknvf&#43;ewNILq9A4daXsg9qbnleWVTdupJ7TzX&#43;ZKwft19nqtSf2tqKfxA1n7I19qaP8AZ7Rz&#43;YhOP7tX4FYA5BrnxrWo5zhal/t6&#43;xjy1o9lUD2ptKZgT5XSpUzg561zv9sXP/PKiPXLiM/6rNZeyqh7U1JYpJJfkoktZf71Z48STj/l1FS/8JHP/wA&#43;y0eyqC9qWxa3ESb0arNlJJJN&#43;8rL/wCEjkxg29EfiRI8/wCj80eyqj9qb8nXpmqkj73YbNtZieJEJz5bUf29C8m942o9lVD2pyXh3W7zVb&#43;axdjuRvlautOk3L/feuQ8F200GrXEjRAJJyshr0IvIIjvlFenVOUwpdKljB/eGqv9n3G3/WVqSy5P/HwtJGjn/lotZCMv7Fcf36X7Dcn&#43;Kr17MNOhE080aIf71VI9bt5RlLmL86BjPsFx/epPsF3/AHqlOt24ODcRf99VJ/bNt/z8Rf8AfVAFb7Ld/wB6l&#43;yXfXFXI9VtT/y3h/76p8&#43;sW8EDOXjIXpg0xGf5Fx6UeRcelVZPFE3&#43;sS3Hl&#43;prRs9bju4N4Cg&#43;5pD9kQeTP6VEUk9K1RfqR0T86ikul/up&#43;dAGeiSf3aX95/d/Sr8dyn&#43;x&#43;dS/aIvRaBHOeFNUgN4dPPEvWu/kiEkTJ/s15DoH7vxinvivXwfnrWqBz0liRK2flp3lR226aWb92oya1bwRPL8&#43;Rx61yvi2cQWkVvDu2zsFLGshmB4x1b/hIrM2unwSfued4rzS3vtUF19ljdvMBxivYXit9JsBGhwGHLY61wvhqG3uPGc0nDorE0zWpSMK&#43;XXLRPOuAR9azxrN5/z0Neu&#43;L7aPUdOcIihkHUV4rIphnZT2NaU/3gqlP2ZeGr3uf9aa0tL1O4l1GCK5mPls3OTWn4O0Gz1C3e6uzu2n7tHiHS7WGdHtFwobqO1DqGapf8vD0iSyiayjG07COo9K8x8QavPpmoSwWj5i3dzXbadeyS6dFbmU7VGN1cZr2gvd3MsiSAtu6U6VL2h04mr&#43;7M2PxRqCR/6w0v8Awk&#43;oH/lofzrMubWS1GyRNretRR1p7I4TY/t7UP8Ans351JHrl/8A8/D/APfVZFSx0Ad7pcv/ABV8P1r2JTyD7V4hYyeX4nt39SK9h1K7/s/SXnY8leKLGhzfifxCLe78iA/P/FXH6hrlxcCOK4bcC2E9qqi6TU9eY3Mu1N3zNVXX/Ls5fMtpBLHEwYGtPZezHSqljV9avZLVbZ2G1RjPeuc0W/Oj6z5mdyuea0IbxNaeSXZt8sCrVho9vNv3oWkdsK3pWZrVqnRXF/BdW0k6ynygM4rg59FOoSy3UM0a/NwpNdPr/laZpqWMTLgcsfeuKkeXcdshUD0rP2Zl7X2n8Q63wjJHDby2rhfNUnv1qPXtTjSPyVRck1zlibiOUTRswfuR3qzLIbufzZVAxR7I19p&#43;7L2iXF7qF4tvEzLGDu4rR1Nntlbc2XrDstTl0q4M9rwSMVBc6lNclmY5J5NdtKocNUbfSvfwkv1WsiMHOK07X96hBoSGNCfWs6oUirGD6VNg&#43;lWYohnpVl4hisjQ1YyBrlq&#43;cfMK9G8cagkejQxh/wCAGvMJMxajZvJwPMFdH4/uVeVIkf5TEu3FaBVObsP3jvJ/eNRau/lWLjH3qtWMYggRD94isnXrnzbhYU&#43;6vWtjMb4cu4oxJAzbWkYV3rFbfTvOQAFTxXnfhHTf7U8V29rzsZvmr0HxlBFpt2bO2b5UX5uaxNDlr7N/PiSQLht3Nc5I5a58sc5fFW55JCpmJ&#43;VWwapqQJVI7mgDttBey0lTc3MAnQpgj0Nc7eXcU9/K0SbIydwWoJJ5Y4Wj3nDdqqwfxu9AFoyIQaiTnOKrfvJW2R10Oj&#43;Hrm4JdwduKPa&#43;zAksfsFrY75RumI6VlAhpmIHBNF5Eba5kjP8LU2P1rnvc0LUdS5qKM8UUAauqq1yYmTAZHzV3xI4eWyY8sUUGuYge4uLtE3tywFetXXhFJtKikXDTRxg810mZwshQMz/AMKiuXngnuZi6fxNXUahYyRxsPU1nWLB7hosfdrp9kc3tSLw7JP4e11LpQrOoq/rWpyaldS3Un35DUL23m3sr7v9WtVpTxmuaqala4i/4kc7/wDTUVjNJsMbVuzSR/2MY/42kztrnbv7wpAaM5yoPtTOkNJFkxJvpZenzfw0AS6ZJGmoIH7165oESmPYzLlhXi9mfMvF/vZr2Pw5HiFJC/Ra4sSdtI4LxPHHFrdxGP71ZPQVd8SyeZ4guW/2qzs/JWlIzZaj/wBXUsdVYpPkq1FWpkf/2Q=="
     data-sizes="auto"
     width="300" alt="Friends from the Knitforce group at the Amplify breakfast."
     loading="lazy" />


</a>

  
  
</figure></a> Some of the folks from my Sunday afternoon knitting group at the <a href="https://www.weareamplify.org/">Amplify</a> breakfast. I have known these people for a couple years, but never met them in person. Thanks <a href="https://www.linkedin.com/in/jana-walker-skywalkerconsulting/">Jana Walker</a> for the picture.</p>
<p>I was able to spend time with old friends. Meet people in person I had previously met only online. And I got a chance to spend time with new colleagues.</p>
<p>Figuring out how to engage with that many people is a challenge for nearly anyone. Having not been to any conferences for awhile, it took me a little time to get my rhythm back.</p>
<p>Still, talking with other people who are active in the space is the best way to gain insights. Sure, I attending some workshops, and I did learn a few things in those. But sitting around playing cards with friends, or hearing people complain about bugs over drinks, is often far more informative. Not because those speakers aren&rsquo;t good, but because they are speaking to a group in a polished way – of the cuff in a small group people share more details and reveal the hard earned lessons.</p>
<h2 id="sales-and-client-meetings-are-fun">Sales and Client Meetings Are Fun</h2>
<p>Working now for Coastal Cloud, which has people qualified to work on all the Salesforce products in all the industry verticals, meant I spent more time working our booth and visiting with clients than I would have in some of my more recent jobs.</p>
<p><a href="/wp-content/uploads/2023/09/IMG_3819.jpg"><figure>
  <a href="/wp-content/uploads/2023/09/IMG_3819-300x225.jpg" target="_blank" rel="noopener noreferrer">

    
    
    
    
    

    
    











<noscript>
  <img class="rcf-image" src="/wp-content/uploads/2023/09/IMG_3819-300x225.jpg" loading="lazy" />
</noscript>

<img class="rcf-image lazyload show-if-js"
     data-srcset="/wp-content/uploads/2023/09/IMG_3819-300x225.jpg 300w"
     data-src="/wp-content/uploads/2023/09/IMG_3819-300x225.jpg"
     src="data:image/jpeg;base64,/9j/2wCEAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDIBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAGAAgAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5&#43;gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4&#43;Tl5ufo6ery8/T19vf4&#43;fr/2gAMAwEAAhEDEQA/APIY9LgiLMZx8tRPbRB3aP5&#43;O1ez6r4Q0jUZFEUQhL/3eK4i/wDCE2najLb2hMpPGB3rmpY6nVOqrhalM4mFGjDPtDL6V0ulG2vlEMdtGZfQiq9xosttOsbW0wlPVQK6C08KBYVuAZYJVXPPrXdSOUrJpdwtw8aW0Ucg9RUR0vUNRV4JGWNAf7tbunWepwXObtPNTs9bDiOM8jn2rqpfvDM5ey0u8tYvs/mnZ7CrceiTuebhxW4Gf/lmn51bt7NpeZXCitKgHOT&#43;G/MeHfcMYs/vOe1aEUXh3SR&#43;7tWlf6VtT3Oh2EeLm6TPcA1zt94q0ONz9lj3474rm/diHS&#43;IUEmLTSdv&#43;1tqOTW9ZuU8tLfan0qbTbubV4XltIFCr61UuNaltW&#43;zjb52cfKK1peyM/3pJHFeS/6xK07OzjjXNzbGVPQmoIYbtbHzLmbZv53U2KW4uY/s1tcb&#43;fvGu72tMy9lUIDo8F3ePK0SxLu&#43;Vc1Bc&#43;HTcz7AY1C9GJqWeyjsZiL6V8&#43;obr9Kz714UKmNpNn&#43;9k1l7U19kdVc6kbVo5WfOBzW1pukPd3CaqZOCOBWJdaQrfMQ3H8LV1OjX8MekLG8ihwelfH0qR9A6pwPiux16DWJLuyQNGDmtHSp7m904fa4WW4P8OK3dWvHKu6DIrasjaRaVDNtQSsvzDvXr0qvszh0qGZa2oeEedHtwOlZl9BbWchmcgRepredLq7tZpbZP3aHDetZOnWkWrlllA2x5DK3Oaf1mpTOb6t7Sp7Q5q58R2eStqFfHfNZdzqdzdZ3F0j/ANlq1fFnhy00W1W4s4trO/SucEt9HDvFmPL/ALzDNdNKsZ1SCHS7e8kdp3Yt2y3Wr8XhO2EiELlP4vm61m/bZjqEahI0LcYxxXbR&#43;H71Yo3bVbZARnHpXV7L2hjcfatYabY/ZYLZwT33VnC1tba9a4mtwO/XNU/EaXOjlNuoJOW5&#43;Wsz7Tq88PmYYx4zuxWdSlTpGv7w6C&#43;1SO6gZI48KD3FUBqA0&#43;JfLQktz8orJ3X0sOXJC1JcmYCARHDbKyqVRlm61Nrtt8kDP6ZFVVvZwv8Ax7bvT5aha5u4V5k4&#43;tVhqM73kUbSn5q0pe0ND225ME8e1JFbsTXl2uanc6Z4rit0b9yzDPpWcPEN/GmwF8j5vxqlcajd304aaHMnUGuKllbpfxZmX1q56&#43;rWl7atEj8kDJFYKPPpf225aSSZIAdimtTwvbH&#43;xo5GiAduWzW3ZaCt3cXBl5hlGCtcr9nf2dM2pU/&#43;fhxfgz4iXElzc21wB5Lgk8VNe6/Hp8zNp7E&#43;aMsB1rqrj4b6R9lIsF8i4GTu9a8ju4LvTfFP9n3MgXaxy3tXT&#43;79mL/l4dFr2vx6potvFIzecHBx0qjf&#43;IrVNGFr5gWTpiufiSe&#43;1x7OM7kD4VvxrY8QfDm4sdMudSkud3l/NjHWs17M0Zy7T7LxJXJKgg8V19x4o0v7AI4S/mEDNcvoOnS6wTGgyQK138HzRybJXC&#43;/avRpZmsP&#43;7OOpgfrBnX1/FfTosHmHA&#43;Zmrctby&#43;OmCztYy0p&#43;7x1rI1HS5rCDckTIifecjrT9D8VSafqUEnDpGeVIrPE4mpiTopL2Whu3Oj6kugG&#43;u3WFgcGMjGaxrm5EElvnP3O1dl468RadrWi2jWr7ZSfmQdq8&#43;1WXaIs9krmALy6jkbcM/Q1oafZ6e9j9qnkCXCn5VJrkrmWQIrZ&#43;Vvu1rw5m0D7TKudjhd3pWqq/vAqHZ/2KD2rQ0jwsLq9DOfkT71XvMEdTWV7JBdKw&#43;VD96uirmU6tMzpYamdNFHHaWuyJPkBxVzS5mJPPFZVtq9uIZEds4NYj67LbC7dDt28rXkUqf7w6ah6L5pCZX&#43;GvPfGtnp63Iv90T3EnBXqayj4yvr6MxebtzxxxXMI91/bLeZ5kq5yC3NdX1b2hl7X2ZJYWf2LVVvIwwBbLAV3Wu&#43;JLTVtBl0/ynUum3disESjy/miK/hVPzfMkdduOK1q4H2dP2hnSxXtKns/Zj/h3bR2mt31uMFVTvXTm3E&#43;oKki8B6wPC9n5GvTvv8A9ama6yaHN2wjPzYFeRVp&#43;0Z6VOp7MPGMdqfDU8SxISF6kV4bBaBpTgYbNe1&#43;JbeafQJoo/vBQTXl1hDFIJGYfMh5r0sLscVUypvMiYI7d6g1yT5UH&#43;xV3UklkuARGQPWs7W1LkYB4UUCKVnNCVX7R8yr0FdFDf2E1r9luImSHr8h61y9tbGT86044d8iD0rNmtM9a8smjy3q1jZ2qIk4JrI1AGOLl6xtVlZ3k8v/AFbAZqW7uXXOV49az7qVTZGQcrmtaRlVMefNlqcMefllp&#43;szanYzpJb7hER94DvVzVrE3FxptxGCRkZxVfxUbz7RFFEshiC5woru2OYrf2/rEUIMoOD3ZasaJc3Oo30g&#43;87Csuea9uoESVXCr/Dirvhx5YL1jtKdP51lVf7o0pfxDuD4d1q1i&#43;0w2zZK43A1Stbq&#43;hLs8knmDg5r1i1nf/hH0ldv&#43;WWa4C7aOQsQp5PpXj&#43;1HVOfv/E&#43;oRqYihdCNp4rDt7cv5zKcFhkitfVkuozvt1Vk&#43;lZ8BldmGBvYdBXdhnoZGxbW1ubNPMjBO3riq02lWU3VF59q6Oyg32EWYtp28g1WubeNUc7cVxOr&#43;9A5SfwwkIM0CsABn7tYVpbl74Ag8N6V9CeGkhvNBjWSCN8fLnFeceNLC20rxEqWqKvmDcyrWntTppER1ufuFqM63J3QVQubWe22&#43;ejLu&#43;7VWSTHFen7IPampcavHPDseLmqM0MuoW2xVZIB6VZ03TTcfvH&#43;7W6EWKLaAMVlV/dASaXp8xsLcFAd3C7q6KA2djZf6bbxSSZxgiueEs&#43;EHmnC/d2npTJnZj80hc&#43;9ctSrUqB7I6D7d4ec/PYIP8AgNUtVi0OayIsYBFOTncBWWIt/wB6rUccezZt5rP2tQPZHS2utxHSEsyRuCbazT5RJHy1m&#43;WR/DRuUc1zeyD2XtCa6s0lhcAhWPSsGy8M7L7z3u8NnoKv3E7Oem2pIieHzWlP2lMPqp0CwhYQMjgVkarp0lxA/l/pUomkePq1H23ZxvrP2Zn9WOj8HMLXRlhlIV1PQ15t4tS6uvHEhKExH5VIromvJRJtVyPpTWZmbdKAW9TWh0UlUpn/2Q=="
     data-sizes="auto"
     width="300"
     loading="lazy" />


</a>

  
  
</figure></a> The Coastal Cloud Booth.</p>
<p>On the one hand, standing around a sales booth, talking to people who really just want to see what swag you have on the table, isn&rsquo;t a thrill a minute. But I learned early in my career that those conversations are just as much 1:1 networking as any other. And I like people, so talking to people is fun.</p>
<p>Not every organization would benefit from a booth at Dreamforce. But Coastal Cloud did (at least I think we did, other people are running those numbers), and it was fun to be part of that.</p>
<p>Dreamforce also attracted currently clients and potential clients we are already in the proposal stage with. I <em>really</em> like chances to talk to those people. Sometimes we talked about their projects, but more of the time was spent getting to know the larger context of their work. That means hearing about the work we&rsquo;re empowering through our efforts. It also gives us all a chance to share personal information we might have otherwise missed.</p>
<h2 id="stuff-i-could-have-done-better">Stuff I Could Have Done Better</h2>
<p><figure>
  <a href="/wp-content/uploads/2023/09/IMG_3835-225x300.jpg" target="_blank" rel="noopener noreferrer">

    
    
    
    
    

    
    











<noscript>
  <img class="rcf-image" src="/wp-content/uploads/2023/09/IMG_3835-225x300.jpg" alt="My MVP Award. Blue at the top with my name printed on it. An a cork-board in the middle for annual pins from Salesforce. I just have the one for this year." loading="lazy" />
</noscript>

<img class="rcf-image lazyload show-if-js"
     data-srcset="/wp-content/uploads/2023/09/IMG_3835-225x300.jpg 225w"
     data-src="/wp-content/uploads/2023/09/IMG_3835-225x300.jpg"
     src="data:image/jpeg;base64,/9j/2wCEAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDIBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAKsAgAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5&#43;gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4&#43;Tl5ufo6ery8/T19vf4&#43;fr/2gAMAwEAAhEDEQA/AF8LwmU8A/cFdl/Z8kkfLf8AfVYnw6iEk7ZA/wBWK9FliQId6DAFcNU7jj5tKaOJpEdD05Haqkd&#43;bW88zLAgfxVBrUs0dxI1rM8abuV7Vyt/qVymc7mNZezqD9qdRe&#43;PQzCCeIbQ3XPWqn/CXWYm85IAH7sDya8zu3nllaQhh6U2Myomdxrp9mZe0PST45cucR8duaox&#43;K75JZnjm2bvxrhhPOeQh&#43;tSwrdTlhGDx1oegHXS6jPe/vJ5i31re8PSKCrBN244zWJ4c8Jy6moea5IT0Fdfa6cunXqQxpIY19uK4atU6aRt2o3oQ&#43;Ru4prRrbTqyhueCR0qRlfKbcY64z3qXzY7lvJl/dyDkVzGg1bibyn80Dy8c4rKhjU3DvuYxt0Wtd5VWOVVIbFc5qmrMnFv5ayL&#43;dAHK&#43;Ktdu7C7VZAxgbjpiuSm8UahGjw2d7IkT/wmrni&#43;51C9Mf2hg237uK4uQyxyfOtelSOKqe5fDrPnPj/AJ5Cu&#43;klCHEjqAfU1wPw5/1jf9cxU/xCmkjnttjsvB6GnV/iGhtatp1vKPMhmi/3SwrlL2GBWKNFCW9d1cTe3dx/z2k/76rKa5mJyZXJ&#43;td1LBe0OarV9mdtLptvcIFZok2&#43;9UjpFsmfnjP41yHnyk/6x/zp0i3aR&#43;ZiXZ61r9RMvrJ1tvajypFj8ptrdCa6OysIIPKmjiiLEfOpcCvKla5xlTJz6ZqT/iYhN&#43;Ztv1NFTLPaBTxR7RLdfZYPMt/s0bdlDCpFvxdWRuJ7pImH3lDV4a0t5/E8v4k0hmutpBklx9TWX9mGv1497hvrO8tENreRI4OWYtU6iF5PMmv4G/ECvnpLidOElcfQ043FzjmeT/vo0f2YH14&#43;hQmnrM7fbI9jjH36zrrw7p18pMF3hh/ErV47YSztaZMznn&#43;9Xf8Aw8PnXVwspZxjua4atL2Zr7X2g2/8Jw4Je6Eu3pWBN4ZjkP3BXr8umWksZXZyaoHw/b&#43;9L2oeyMX4bD52z/zzFL8Ro/31qfrXG6L4qk0SNfI&#43;/jDZGam1XxTP4iKedj936DFamlWlUMC/7VlnvWxe4xWOetephf4Zw1SMHDA4rbm8Ql7FbZYEGBjpWQRx0qWzigkuP9JYquO1dxymhba1PIvlxWsZKj0pzaxqGw5tVCL/ALNM8vTbYb47h93tR9stpE2PcyYrPULFe51ed41RoUTvuxVZdSk8sh40b3xVyWPSX25uJG/Csu9EEcmLVmZPejUCJZQpJx1qKWXf0o6iojWj2A3tL/48DXe/Ds4ubj6CuC03/jxrU03xLJ4cdnChvM9a8Otud9I9u82iSWvJf&#43;FqSf8APBKP&#43;FqSf88krl9kdRxvNX9O/jqh9og/vVf06WOTfsrpNKtX92S3fRaopGm0&#43;tXr7gLWZg54Brvwv8M8yqWRJHH8jxbvfNSsbRMfIv50yDTb67/1UDn3Iq9F4R1Ob&#43;EL9TWlWtSpmfsinIbDH3R&#43;dR/6Bn&#43;H862R4HvP45Uo/wCEDu3/AOWyVn9Zpf8APw09lUMZ/sA/u/nSf6Bj7q/nWrL4Cvx9yVG/GqFz4N1a3/5Zb/8AdNafWaX/AD8MvZVTLmltxvCRfrVA96u3WkX1pzPA4/CqTAqORium6ZnZm9pf/HqtUNe/gq3pefsi1Q188xV4dX&#43;KelSMbmko8wUeYlM6valv7Wf7orZ8OyF5JeK9d/4U5pf/AD2krC17wVb&#43;F0V4JC3metL2hwmDFbC8u4oH6Ma6O40vTdKtA/kq8nvXP2RI1KHHXdxXTjMksaTqWO4HDVleoa2KialdRhG&#43;y&#43;XEejbeDXQQy7kB9RV/XrNDosLrtCnGQKzbVkEQGK5qrNaSJU5c76NnPBozRWR0gODUnVqjozQFhklvHJlXiV/Yisi&#43;8K6ZeKd0Ko/&#43;zW1nnrSN90n2p0q1WmZeyPMJ7EadNJBG25Fauc8Ry7PKNdTqf/IRm/3jVAeGpfEZ2Rtt8uu5Puc1U4M3p/uCk&#43;2t/cFegj4VXZ/5bD8qlHwjvD/y3H5Vr7RHKfS&#43;K4L4k/8AHpb/AFNegHpXAfEr/jwg/wB6sTU84sf&#43;Qnb/AO&#43;K7LULN53Qwnaw71xtl/x/2/8Aviu85rOqzppFURXj7Y57gtEvRashRGMYpaK5TYX5KST2ox7UbPeg0IwS8gWpH4ejGDRQAfJSN/qjS/wU1pE2Ee1AHm2pn/iYTf7xro/AgHmXH4VzGof8hGf/AHjXT&#43;A/9ZcfhXccLO8jAz0q1EKqx9atRVmYnUZrhPiT/wAeEH&#43;9XdnpXCfEb/jxg/3q0GecWv8AyELf/fFegD7tef23/H/b/wC&#43;K7wZ&#43;5XNiTqpB3qWosc1L2rI6RDio6Xkmk/2aACijGKj5zQBJUL7Np&#43;lS5&#43;aomxuO70oQM821DH9ozf7xrp/Af8ArLj8K5PUD/xMJ/8AfNdZ4EODcfhXccLO8zUscuKoGXBo87HeszI7Lza434g/vLCL611P7yuQ8e7xpkWf79aCOAtf&#43;Qjbj/bFeiCLYQfavN7XP9o2/wD10Fd7FqkMjNAXG9TtrX2XtB&#43;19mT96KMhec5oHX5q82rS9md1Kr7QT7jUz&#43;KpJCNjVCCOmeaRqSvjFR0c0EcUAR/JVK5u41nWEn5mBxVz7iVwPi7VXstViMf3kUj867sNS9n&#43;8OHE1f8Al2ZV4PMu53T&#43;&#43;a6vwPx9o/CuPtHMlpvPVjmuw8CRPIbn8K2MTrZZcVRluuetXprKTk15t4o1q8sdS8hA0Y&#43;nWsQPoWuI&#43;IvGmRf79drcSJEhd2CqOSTXkXjnxdBqdwtpD/qom&#43;96mtRnOxy&#43;VcxHuGFJe3M0V3K67tzMG&#43;Ws/wC3xCRX3jg1ebV7UhcumaZlVO70OVr3RY3kz5lOmle2b524&#43;tcrpOryLgQyqF/2m4rQ3RXDebf6knX7iGsqtSma0qtjWTU4vLPmde1ZiXU32gzM/wAvpVqO50WMY81W/wCBUpvNIPdP&#43;&#43;q5rGvtSwNSjkT5XwfepIGmuZwocFPWs6R9IdOJ1X8aUa5pWkWjP9pR&#43;&#43;0HmtKSpi9qTa1qUdlbyRIf3y4/WvK9buZLq&#43;eSRtzdKva9rjajqZuYWIibgDNZcl9bm3ZHX99n71dxxdS/Z/8AHmtd98O&#43;TdfhXm9vOI4F5FdL4S8SJpOoiJuY5jy3pWZ0HrMsdcl4r8OR6rB5gXbMv3WrsIJFuEV1IKkZBp0sAl4xSAw/iZqlzaQxW9u5RJM7sV5JLGsqcnn1r6K1rQrPV0T7VFu21hHwHov/AD7/AK0wPCDpyY&#43;&#43;aiOnp/er3c/D7Rf&#43;eR/Oo/8AhXejf88m/OgDw4W&#43;0YDsPoaQx/8ATRvzr3H/AIV1o3/PJvzqP/hXOjf88m/OlYDw7Z/tt&#43;dGw/8APRvzr3D/AIVro39xvzo/4Vpo39xvzoA8P2k/8tG/Oopbbd1fP417j/wrXRv7j/nSf8Kw0j/b/OiwHhP2QYxupv8AZ/fNe7/8Kw0j/b/Omf8ACsdK/vP&#43;damZ4aLdhxk1IEMZBB5r2z/hWGk/3pPzqM/DDSf70v51mBmfD3Wbi7tWtZjv8v7rV6LF05rH0HwjY6EW&#43;zbju/vV0QixQaGpUUlS1EetAEXNFS0UARYNGParOBio6AIse1GPapaKAIse1FS0UAVjmkx7VLRQBFiipai70AFFFFAH/9k="
     data-sizes="auto"
     width="225" alt="My MVP Award. Blue at the top with my name printed on it. An a cork-board in the middle for annual pins from Salesforce. I just have the one for this year."
     loading="lazy" />


</a>

  
  
</figure>Being late meant I missed the formal presentation to MVPs of awards, but I still got mine.</p>
<p>In part because I changed job during critical window to arrange flights and hotels rooms, and in part due to lack of experience, I made terrible travel plans. I missed day zero entirely. So I missed the MVP Unconference, and some time with colleagues getting our stuff setup (I actually <em>like</em> basic physical setup projects). I also flew red-eyes out and back – I don&rsquo;t know how I got through the second day I was there.</p>
<p>Next time I need to plan further ahead. Make sure I arrive on time for day zero. Make sure I have a reasonable flight there and make sure I have a reasonable flight home again.</p>
<p>I also didn&rsquo;t play the swag game aggressively, and so I didn&rsquo;t get as much &ldquo;stuff&rdquo; as some people. Frankly, I known I don&rsquo;t need more stuff in my life, but somehow having still had room in my bag on the way home made me feel left out of something. Oh well.</p>
<h2 id="to-sum-up-my-first-dreamforce">To Sum Up My First Dreamforce</h2>
<p><a href="/wp-content/uploads/2023/09/IMG_3829.jpg"><figure>
  <a href="/wp-content/uploads/2023/09/IMG_3829-225x300.jpg" target="_blank" rel="noopener noreferrer">

    
    
    
    
    

    
    











<noscript>
  <img class="rcf-image" src="/wp-content/uploads/2023/09/IMG_3829-225x300.jpg" alt="Several Salesforce mascots waving goodbye as folks left." loading="lazy" />
</noscript>

<img class="rcf-image lazyload show-if-js"
     data-srcset="/wp-content/uploads/2023/09/IMG_3829-225x300.jpg 225w"
     data-src="/wp-content/uploads/2023/09/IMG_3829-225x300.jpg"
     src="data:image/jpeg;base64,/9j/2wCEAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDIBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAKsAgAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5&#43;gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4&#43;Tl5ufo6ery8/T19vf4&#43;fr/2gAMAwEAAhEDEQA/APMrXS1uoJZJSwdB8qhetQizuOggk/75NfS0mk6LZSfJaw49lFSBtETrZx/98VoZnzFLBMhG6Nx9RS4I4IINfTj2vhq6x5tnD&#43;KVk6z8NPD&#43;ujdZusEmP4KftAPnmuq8C&#43;Fz4i1&#43;FGGYYzuf6VreIPhNrOmK8lt/pMS9NnWvRfhP4cl0fSGmuomS5kP8Q5xSuaHodlaxWVpHbxrtjQbVFWD95adUUn&#43;sWswFk&#43;c7ac8SOBu7U6igDxX41RCS7ssfwqa8rtwen8Nev/F6LN1ayY4wRXlwiArppAR&#43;W/8ABVW9sUuYv9utADkU/wAtNzFa1A4xTLZXYYfI6HIr1TQ/Fy6lo5S5I82IYI/vVyepaQ11aGdEAda5a3uZrS4JTI29RXNV/h&#43;zNcLV9lV9ofZzWNtCMmFT&#43;FQH7JnBt0/KobldSuRhGEQ/OsyXQL2T/l/cPWRkbT2Gn3A&#43;aBR9KoS&#43;GwG8yznaI&#43;mazjouvWvMN55n&#43;9TodZ1Wyfy7y0LgfxLQBanu9R0iMmcCVMVW0Xxal75huNsSKSBgdK0TqdhrNq0LtsfH3WrltulxSta7JOXzkdK6KcFMzPQLfUbS4A8udDn3q0zKcciuCt9OgW7Hl3Ix12A8gVLLPqVqyyKzugb5V6/nSeH/AJA9odzmiuOTxTcxXEcE1scsQNwFdJZXwvEbAwynBrOpTnA0OG&#43;Klp52kxTAZZHryH7O/da9y&#43;IkLyeFrp4xlkXdXzg2o3srbFY59BW1IDoRbNjtS/ZsfxqKh0fw94j1Zh5MMu3&#43;8RgVuXvw58RwWL3Dn7g3fep&#43;1AzSoVfLMnFc5caZF9vV&#43;PKbO6pDYajJI6SM4ZBnrU&#43;n2&#43;LRnuS3yv3rT&#43;IZn1n5YqN4hnNVYbe7BBeWrkjqqjcwH1NcJoNFMZEZvmUH6iqFxqUcL/NOgH1og1S3mPEyH8aAIdQ0C2usmJRHJ/eWoINEt7O3ZpFV5McsRzW6sqkZBB&#43;lLJHHJGd1NOwHm0tw9rq10AhAZPkO2tDT9VvY7MSzBWjXjaRzW1c2Dyz/ALoqfZlqG7t5zA0Rsw3&#43;7XeqkJnP7Mzzr8D3cDS26bME59DWraazYDPlEJv5OfWudk0h/L2mFlJGOlMtNJCzorvhgeVIp1adK2gv3h2eoWser6VLCGG2ZNua4nRPh3pOh3HmXaLc3BOVGOldXYGRIxbx5LZPPYVeMUVjG0znzJPevOOkBauluBCkcCj&#43;6MV5l4/&#43;IkdjL/Zds5fb/rGX&#43;Vdre6pqFzod/NbwNvClY0HXNfM&#43;p2WqDUJmvoZIpGYsxkGK1pgbyeI47lH2R4JHWsZLuS8/0Zf72eKom4Ftb&#43;SrKxY/eFO0c/6ZtD7fc104X&#43;Ic1X&#43;GfVtzqN3cZS3URr/eNVBpU8zK813I6t1Arat7UY3OPvdqxfE9zqSQrZaVF&#43;&#43;f&#43;P8AuiuE6SwdAsm6At681A/he1l5iLRkehrzy9vPEvhS9jmvLl5I5DzzkV6Bpnii21PThIj7ZNvzD3rJuxqqV9h1rpl7Y3Kt9szAOoY03WPGmiaKv&#43;kXibx/Cpya8&#43;8aavrP9nzIkzxpuyGXjivIpUnuZS0kzSE92NdWGw1SrS9qFWl7M90uPjJokcn7uKR/fGKI/jDpkvWCRfpzXgcsWxtr5Vh3qOOUxnGfyo9kZH1BpPjvSNVXakqhv7r8GtGbZdurQxjJ6MK&#43;Xre9mgGQxx/eBrsvC/xHu9Ju40uZGlt/Q9qz9mB9D2dslrDg/f7mh4t7GaTlF6Cquha3Z6/p6XVs4YMOR6VfuuLWTH92kBWUZh3IVXdzisHWdL0/WI2tNStU3OMLJjpUmpQX8thb3Fk53RHJX1qawuxrVk0cy7LiOgD588W&#43;Bx4c1MxyOzQucxsorJ060H2gxQ20kztwDivoHxBp8N9ocpvY1MtryrEdq5jSfFPhrTIQDbLJL/eCV00mZnsmKY&#43;xQXbAx3NPzXJeMr29jhS3skcmTqV7VzGhm&#43;LbzT9XT7FwzIc1h28cVhb/ACfKgrFSOeMyzOx3q2GzVtNRjZdkvQ8V5mOw1U9fA1aVPc1FNrq9q0UuJIW4rzPxj4WXQ/30dwDG7/Kg6gV6RZNbww7Ydqqeaj1&#43;LT7/AEh2uVVzGODWeW4&#43;th39XfwGuOpKpT9ojxi6sZJokaNjIMcU6HQ9yBppdrGtsRRwxkR/czxUEh&#43;TNd1XE1OhzUsNTqFOXQXRf3Nwp9jWbfWUlkil&#43;Gatl5i&#43;GBqHVStzpwZyN8fStaVUyq4b2ZrfD3xncaDqcccszfZXbDLmvpSG7i1HTBcQNuR0yDXxhGWjmUjJOeK&#43;mPhpqsq&#43;GY1vFZFA&#43;XNdNQ4rHeWUYjs1DelVJbEQXy3ECgA/exXNeI9T84r9mv1gC9t2KpaJ4pubefy7qUXEJON6npWRp7JnWazbJLZ3KEf6yIjFfMepP9gvZI0b5kY5WvqSYrepHIhzGy189ePYrCz16e0VFBkOWk/u0GR9J7gRwa5rxlrkejaHLO2N5&#43;VT71laJ/bK3USySPs3fNu9Kx/is4njtbXPyqd7/StDM5D/AISeOKykW6kDGTleKoRapHcfOh3VheJbjT0kRLJiwCjOfWsG2nl&#43;fy3ZfpTq/vDRHoP9rlBsVytZWo69IsL2yzZV&#43;orm4bu7kYgt&#43;dMk0&#43;6&#43;yy3TNkKa5fY0zp9rUsdDpST3sbTOyxW8fDMe9Z2qrLFK7LvMTfdPSrOhJcx2IeU7oGbla27t4ZUAkjG0DiipamOlVOPjt7t494b8KWTb/Zr&#43;cT52eBXQt5CqfL7VzN87i4dpcYI&#43;XFa4WmqlQ1qs2fB2mQXl6JblNyxHp6162t/bPZm0hkMMuMIOled&#43;Bogbd5Nw61S8Raldp4gKRzeU0eCmO9ZYqr7TEezpmlKl7On7Q7C58OToJJb64aQ9fvVleVJaMGspWBB5TPWsifx3f3tk8EmFIABcVhx69Pb3SSGZmCn5q5qdOoae1PovwjqnmadHBcttlYcKTXl/xTGmxahcq6Frgj5T6VteAtTh1l5tQjZ2NqmQhrg/iL4hh1zVXeJCm0bTkYOa6qZw1dz6heGJeQADXkPxMjuZ9ZiSBgqiMhs17BJ/q68l&#43;Ityltq0Rf8AiSummcx5Vp3hp9T1OSN5gqIfmzVTxDpCaHeBbaXejd6W91aez1GZoHwH9KzJ9TluIzHM28DofSsvZ1PaGvtKfsyH7RIOjYNTwPeSQOisWjP3qzvM5rU0pz5MvzcVpYyuX4tRkigSGNsrj5hWoZd1opLfNiue8vy2XZ95jWhNdNboEde1cVWkd1IGlkEch9BXPyXPngo/UVptekxuifxVhywy7icGtcLTM6p03hDWBZSNA78M1amu6bHcyNfJKS38q4BZGhlBGVIr0PR4Lu40dbiVg0eKWJpezftDXDVb/uzHh06KUB2kG3uop97ZWMQJK5ZhwBViWzikc&#43;WWT6V3Pg/4cG&#43;jh1G5PnJnIVjWSZpV9mjU&#43;FmnR6R4avbq5j8sPk8j&#43;GvIPEcsd7r969ttaLzDtIr6d1Wyht9CuIY0QL5JUIPpXyxfwTWV9LsjKjec7q2OE&#43;yJZE8s/NXj3xiV1s7W6iTO1trGsuf43E/6uwP4mte81ODxn4WLkbWYdD2NdBkeI6gcsG/vVQ4ArpNf0RtOsg7SKWDdM5rmUUu4UdSadwsNNdto2kwjQ1uncZb&#43;GuWbS2SYKZUx1PNdNYlTaJCkm5V6igCK8jR4QkEQ3g1njzZpSk6ldg71vSWU6FJ7cBtvammwu9QlO&#43;JYhiuWrV/eHp0qV6ZzUG9bokBSgq7JrLLx9khOP9mrq&#43;GZEc/v&#43;9WI/D6RjMmWop4qkg&#43;q1DIib7dcKv2SP5z6V6W2mSWfh9Ejj&#43;UJnArloLKO1mjkRPuGuuuPErnTvLEOWxiuHHVvafwzqw1H2ZzFppl5OVkWFtpPpXZWmu6lp9slvGxRU/hrmrXX763iKiPvxV&#43;yuJ9QR3mG1qKVUyxVI07zxLqE0RBlPIrh79jeTEzKrc&#43;ldRLDxisO9sthyDXd7U4PZVC//Ytiv&#43;p05V&#43;ozWxp1hJHF5SJsjPZeK6yXTo4vux/pRFEAfuCtbmfszzvxbp1tpmm&#43;ZPCZN7dzXnpkWB1mit1Udiea9x8WeH017TVtlfa6nNeXXXgLVkYxlgY16c0e1pmnsahyM5lkuGm2/e9K1NBa5jumYxny2rrdK8MQ2wQXEfmNXQxaVbxfctlC1yvH0zqpZZUOesnlkutnlnZXRR2sfl7l&#43;/V2K0WPkRrTZIj5nCflXm1cV7Q9jDUvZ0zPkscjKp81SxWBkTZs5q&#43;A/C4q/bDH8FcvtTT2Zgvo/l/eSpEsIyuwp81b08bSelRfZsDPei4aGFJpgz9wVahgSKPGyrZiMfz/e/Gk2vN0G2taVU5qtSmZ9zHxxWRNETJzXQyWjo21jmqM0ODyK19qcNU7cai8n3wtSiVP7orLj461L9oTpXTVqnm0i1LsJylQmEMORml8yjzK5qtU7fa1DPuLQA5CBaj8mTy8bBV&#43;SVC6o1Jx5mN1cp1UsVUMzy3jPNSeVxmrF3blwCnrTli&#43;UCsjT2tQrJF3qXDiP71S5A&#43;SkyKyH7UgjJzzmnyciiT/wAdpMhY&#43;OaAuVsjOw08yuF2RKn1NN&#43;&#43;c7aSREC9cCtTEjVjtPmHmm/ZhP8AxqRUcqcdaI28tNmK1MzUjl8zipBGEfLVRBw6YqzknvXUcpN5npQTSR9KZJ92sqpsSJ1yVo/5bb9vFICfIoY/uT9KyNCXcD0Ipnmc1R00kiTJJ&#43;artZGgSFMfdqsc5&#43;XNWaABv6VkBEeY8HrUSpszk1LJ/r6iuOlAw8yPZ96qskqY&#43;aou9R3X3B9a1ESEx4z296jykn3DVe/YrAMHHFR2xIgyDWnIZn//2Q=="
     data-sizes="auto"
     width="225" alt="Several Salesforce mascots waving goodbye as folks left."
     loading="lazy" />


</a>

  
  
</figure></a></p>
<p>I met great new people.
I got to spend time with friends.
I learned new things.
I have ideas for future projects.
I helped support others.
I had fun.
I did not come home with COVID.
I did not come home with DreamFlu.</p>
<p>On the whole, what more can one ask?</p>
]]></content:encoded> </item> <item>
      <title>Supportive Open Source Projects</title>
      <link>https://spinningcode.org/2022/04/supportive-open-source-projects/</link>
      <pubDate>
        Sun, 10 Apr 2022 18:09:38 +0000
      </pubDate> <guid
        isPermaLink="false">https://spinningcode.org/?p=1846</guid>  <description>Our project demonstrates that building a supportive community can yield great results.</description> <content:encoded><![CDATA[<p><figure>
  <a href="/wp-content/uploads/2021/12/DataGenerationLogo-300x300.png" target="_blank" rel="noopener noreferrer">

    
    
    
    
    

    
    











<noscript>
  <img class="rcf-image" src="/wp-content/uploads/2021/12/DataGenerationLogo-300x300.png" alt="Data Generation Toolkit Logo features Loinheart Astro with bits falling on him above the project label." loading="lazy" />
</noscript>

<img class="rcf-image lazyload show-if-js"
     data-srcset="/wp-content/uploads/2021/12/DataGenerationLogo-300x300.png 300w"
     data-src="/wp-content/uploads/2021/12/DataGenerationLogo-300x300.png"
     src="data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAABItElEQVR4nOy9ebNtx3Uf9uvuPe8z3undB&#43;BhoGS8R1KUoMG0BmeQnfI3SP5IqhyLYrnKf&#43;Zz5AOkKpaVOFUpf4vEtiLJsiOSsgQSAEmABAG84d575rPn7k6t1b3PPfdNwJsAMGS/Ou/ce&#43;4Z9um1eq3fmiV&#43;uX6h1y8Z4Bd8/ZIBfsHXLxngF3z9kgF&#43;wdcvGeAXfP2SAX7BV/BFX8CLXF&#43;99S9gbQfAwloLIfb/Kna/W8v/X/6JfuQ/SkAovPPO//I5X/nnt8QXfQHPe73xxrcRBo7YOwL339Na4b6zEEJA7H1/yzwCWAHmFIu9l1q62QBaD/DjH//PX8wXe0Hr/zcM8PWv/3cwXQpLp3ZHZ/5&#43;ygIKsCEsQgAxgFAIQdJP&#43;Zdr60RFC6AWgu4F/az9zQkJYgVhoFSNt9/&#43;N1/o931e6&#43;eeAb7&#43;9X8Bo1sW8XxgHaWkJ25sBQYAprCY8j0wApDT34RwKtBaEPFrAFsAKwBzITCnewuxAWztGcG4DfP/S4kf/OBffpFf/5nXzzUD3Lr5bf/TTmJLa5nwKYAxgGsAXiPNAOBVACeAHfu/h0RC/1rjT38JiCWAewA&#43;BPABgJ8CuAuAHi&#43;FIEYQZncRFnjnvX/1RXz957J&#43;bhng1s0/3v&#43;VvkdgrU39KX8ZwFcA3ATwJmCJ&#43;EeAGEIggRCKYAArd5Ya1noRQqe8ArAGcA4IYoL3ALwL4H0AHzvpIEoAHSMH4T7&#43;nXf/5Avbi2dZP5cMcPPNb/mfBJ96AJEnPBH6FmC/7u77U48hP0dKZcNIII6ElZJlv9Aa6Dqg0xBGW88MxAiNZ4ReGrwDiLfdPUuFBSAaLz1YFLz73p9&#43;kdvyVOvnjgGc2Le96UbET7yoJ6J/E8BbgP2KJ3zumYNPvI1jmMkU5uAAiGPAGqBpIcoSYruF3G4gqgrQpsd9PSNsHSMIkgLfBfAfAXzfq4aKsYE3Od559&#43;dLHfzcMMAtPvWXl&#43;uRPYn86wB&#43;E8B/Cdi/73X&#43;yBNe7l6kFMx4DP3qa&#43;hu3IDNc35YdJ0j/sUF1O1PIM/PHRNYu/dRfMobDxB/Aoj/F8C/98xw22EDoS8vzuKdnxNpoL7oC/gs69atf&#43;bp6BUuO7DswIv43wHsPwLw&#43;17vTx4gPqn7IIAlBnjlFejr12EnE9hBDptmsGnKzxFFCbHZQLTtPgN4jxBbDIlnrrEzJ0XngCObjnqHRgVwdPybOD//7he2Z591fekZ4NbNP7r0WDu7PvQE&#43;FUAvwvYPwTQn/yB/05XJRuJZ2KAwQDm8AhmNAKiyGEAkgBlCblaQc1mkKv1/Qywexd/i7xqGbt7lv2NEKwK2n2X4tHRWzi/&#43;N7nsU1Pvb7UDHBzT&#43;yLnvgCBxb4qhf5RPzf8qg/u5/4zhsodjerAmYEelx2LeR6A3V&#43;DnX3LtSdO5AX55BFAWj9qEvalwa5B54T97sgJii8qjCOfySODn8LFxdfXknwpcQAr7/23yKO8/1YVY/0DwF8DcB/Adh/6Ew8fqwX&#43;TtiS&#43;l8/UpJvhkIaBXA5AOI6QSCpEHXwaw3wHbrgGBds0Sw1lzZGCKmMZYf3xMMxjuPZt5M/HNAEC74AYALxwjOX0AvqWuBn/70y2cqfukY4Gtf/RaMuSKBe8fOqSf&#43;HwD29z3xJ14lOHDABFdI0hBpEiEMFeI4QBgF0Nqi7QyskJBJDBXH6NoOXVlBNw1R2L0JRwRs70p2bkJtUJUNqqqF1nr/2qwX&#43;wvvLyAm&#43;AsAb3sLoQQ8OBQWUmj84J1//QXs6qPXl4oBnHPnCvhS1rKoveFEvf0DL/Jf9zo4uCryBZIkxOHhEMfHI2RZxAwQEQMYoGs1e26EFBwX6rR2TNB2/HqSFL03uQ8kCT69Lc7OVri4WDMT2Kv4wLJTyHkKfwLgO4D4c3ePn5EJuQ8Q6XPf/RJ5Dr80GODSs7eP9JnIX3EI3/43AP6BJ/7wfuLDM0AQKAwGKcbjDGkaQimP3az7e5yECAOFrtOw2okaYw2rDHot3SspoQKJMAyQZjFLkrJssN3U7nVX8WGPC2J/XQeAGHiCEzCsXHCJ&#43;MappaOj38L5lwQXfCnyARjs9WIXkNaJ9QOP9P&#43;B1/e/7h0&#43;yaMTWSwTaLUqYIxBFO3zt4BUElka80nfFjWfbK0NP5eYQ8kecDocQapjkCd8WU3dMbJ7wDbo39zt5dCZpjZ2oJQZ4a&#43;sxQ99gKl12MHi1s1v4Z13v3hfwReqAmgTdpfgYbN1BD5x&#43;t7&#43;njP12K17tA/2HrWYkB74sXdOeJEuJaSUiELFjNB0Bi2pBGPQs56wZseIwkuEJA4hycarGhRlDU044kETcX/1TqNz7zb&#43;S0D8JYDvC4Gz3nPoElLc7ftfYETxC1MBV4iPXdw&#43;8ybdbwP4Rw7tM/EPP5X4bNtJvjcW0P7W0b0M0AYxOqnQEKAzErUK0akAWiroKObnEHF1p93rpERrgLrpUNUNmlZ7S&#43;CxxN99l72IJKkE&#43;l7am4k&#43;tNznqFgcfoGm4hfCAF/76h/55BxeLpLnxOdr3p//jz3S/5WHgb3d4hMumfA2UM65E9It5JsNAiCKYbPMef7SHIZMwTiGTVLnHQxjmMEQiEJCify2Nklgkww2DDjBhF8ThLBS&#43;Qv&#43;TEzQxymGzl/gmMBjgtoBR5d5JITF0dFvfCFOo8&#43;dAW69&#43;ceXxL/07B340O3v73n2Xn2kZw&#43;9zRfAEuHCCDZNAO/WdbfMMUMUwWY5zGjMhKXXIAycF5DQPyH/JIEgHFDVQKBgiSHi2LmQ6fWJf09isF2umc8pFI/Uovuew4E3WYecZAi03nPY9OqAnUZHv43zi&#43;&#43;80P2/f32uDHDz5rf2f5U&#43;K&#43;fQWvwagD/0xH8LwEsP8&#43;zx8qKeTzcRJc&#43;dLz9O2MvHLt8kdSdfKQ4CMeFSd&#43;KZ8HSCNYd/ndoIQucSrir3&#43;ixzdDXGMRcxGUuTiJ/L70OMQxKBGEqKvYu8L/PUfc/Qf58DrxIijwW851DY3kw8OvzNz1UdfG4McPPNb/cSD/c5d74B2P8awH/lQ7rH3qS61Pe9fifCK7U7xSzaBwN3soPg8u2V6nM/3e/WQJDu1h0EiXndMdGIgMK/LzNAU7vXReElIem9GFtIp1qICUiF0M&#43;sSjzj&#43;c/epYzdx7ZejWVeEox797HHBH1egWeC38bF5yQJPhcGuHXz244WdveZmRfx3/SRvD/wnr2pPy2O&#43;M6154hOGx/5DQ99Nhc9TqKaRD3rZwthXJKHaBsn1unGxG34nv38dB3Sn2DPJPw8ay/dEPS4B3zC7ul9xg2RY8D&#43;eli19O8nfeBCPEwaqD1/QR9MgpcGZY8LyDJwvoIXzwQvnAHuS93qT/4NAL8H2H/izbw3fJj1EuyxmA8B0r900knUD0ccwiVmYGLSDY5Jdtk9JLa7XrwrJ76JeYhI/n1w/41USI8b9tUJEZMYQzsGcp/nGYKYg5iIpAC9JnJ4g6&#43;FACgzqWOsh4SWY/99Dx0&#43;4AzkVe86Fp7bPg&#43;H0Qt1BH315rf28bLyEbRXHMhjff9Nr&#43;&#43;TXfx&#43;p&#43;NDB7yGQ6fDCfDRRtMGlwVQVZBt65A73VhM09a65xo6oUQckhBRCEEqwvsCenwmdifVevVhmbBWG9heipBaqGvYqoIhJuAUstKJM7pGFu6Z&#43;xwvlWynHcYoC8j1GqIqveRxoN/vxdB7NUP3B44ZkNj5CBBbbzHwAXqRWUYvzBF0ax/wWXbw5P4L/67T9/Z3PDOk&#43;yJ/B&#43;4I0WcZTJZ5Ha2dKqBTVbiNRdM4k2004tMtkhgijpnYZLMbIoI1UEIgIm2hBJQ1MHWHrtHsKJC4dBwpOrVKQUPBQEHFClFGzCNRVDW26wLNtoApS6AsOW&#43;AcwuJQaWHLHTfqxmjOcGEmaCuLpngUiIYDwQ/AvDXgPh3AP6DjylshbjMPn5RTPBCVMBDiJ94B8/vAfgnPnXr5SvEl16nJynMeAIznTqRzCBMOb1e15Blxfd8VggAHhzAHh9BHR8inY6RZTG4PIQY5PwM0WKOUbnBiW1xKlsc6gZZsUEwn/Et3a4wbgocosWJ6DBuSkTzFbLNFtdC4I2jAV46HCDLUuggRBsTUw7Y5KRrI8Yk64HDyURg5XDFjjFYFezZCD3xL6VBDw7HThIKwgNzzxgaO3XwFi5egJ/gRccCSMLGsOzD/w3v03/rPrHvxCYj7Hhnx/PmEaAj04w20OtiQvQ2iVkymOEAJokhQ4VUtDiGRtZW0Ksl5nfuAhdzpEri9GiC0yTBMHVYodDAstQomgaBVJgMYhwcRMjSCNtNidvnM9S1wXHW4eU6Qlh0GHcCqqgRbA3WKkKVjZgZdL6FWS1gN2vYHpcoUjcuZ5xBos9IIqlBqgt15bKODB9w6Q/CSz5OsAXEmi0DsUs6tS9KWD93CeBA3y6iF3m//m95tP&#43;7HgBmV8Q&#43;gaZ8wCeaJACjeuKdunY5elXpQDnp9PEIZjqBzlJ0poNZzSHO72C4nuGarjCqC9j5DNuzC5iywCSL8fLJBEfjAZIggBSk5zWUNOwPGuQRjo/HOD4aYZDFaJsK84sLdE3BDBMLA73dukyh2RribI14VSGrDVIRIIxTyNEANs9g6Ht4ScVSqgeKSl3iA8IdzMz6fnDYWwiZN0MKCAaGLusY4oWAwufKAC5/Dz0DkHSZOCeP/ccA/qFH&#43;/mVz&#43;2RNKFxuu/NqK51hCfdSaBwNII4vYbg&#43;BAqkrDLC&#43;iPfwb78YcQ924jXC0RtxVsVWI9X2GzKqBgMZ0McHw44aCO1k6lMhM4KM/5A6NxjixN&#43;KCt12ucX8zQtg3iNIDWHeqyhtAGelOhmxUIVxUG2xrDssNICgynOcLDCbos53gDmtZhBGKEfb1PH9m2DCzZH3E1rtCrg9R7DumhGQQWgn0FgqOIzzvP8LmpgK9&#43;9X/kNHu3uEQrvUzkwO94AJg/lOlI1BsXZXNeug5ghN/BZinsaAxxfIj8aIpJGiJclFgXC8xn91DM5mwFlEGIu3WDiGP9liVxlsQIVMBRPRcdtBzjD4IQdSMRRgpRFCKKQ7REaAJ6RYGmbaCN4ZuB5TCxsQZNU6NrKohKIBId4k4jiQTCNsVSJfggTnBnNEUhJKef2fWSvwcxgQ0jBqRkWaAX/w8GlpQn/uu&#43;FuEuLC4sxw9INbgilJs3/xjvPidQ&#43;NwYwJodXQnnRD52T3r/m/7kDx9KfNqEzp0YlvN7oMmOhgwIkWdIY4UTNHjN&#43;35mgxg/yyLcW0nUHQFug2Jbo2JrUEKKgOP8xlo2/ZI04rBuyOYgEbdGpwUTXwWKT3pVlajqEl3XcRoYET8bZEiiBNW2xrYqUDQ1AhtwWDkKLQayw7AuMbgAdMG2Ay7iIVbXYtR5DruYAUXhTrz3NrIP49FRxd5EfMOBZUE4YG0tPnJBJGE/QzDqM6/nogJu3fqn&#43;57b0Ds43vKi/&#43;/74o3oUWYnI2MymXpHjlKcum2ODmHyHLKpMdws8AoaXFMWAamGrmV9W5U12qaDEBJRFCGJIyiyGrx9H4UBi/coCjip01gNbTpsig3KukQQBPw6aywzwGa9wXq9QVW2CMIAo9GQpch8scL5&#43;RJ11fLv&#43;SDD5GDAKgStgV7WkIsaeWeRBiFsnKBNM&#43;jI4QJUBZuvbDHoxzLAfvwg8aHjuU8oqXopcHzyfOoOno8EsLu3kWDRb1/zYv8tT/z4sT4H78WztDEqdcQ/PICJQmC1gDi7B9VVCHCAThpsVms0dcWEHQ2HnK5ltMVoNMB4lHP2zmqxZT2&#43;mC/xobBYEUaIA84TzAcxmrZE3dScREpMICHg&#43;M9yXkBZ1Tg/m7M0SZMUm1WJLZmgNoARAkEaYnCQQUFieVGjWVuEQYzrA4kDYxCvG8gowHk2QSEENBG8bh4XPdxfwhP/1B&#43;kO4A482qBy9Qv1e2zrWeWAHs2v/ANGK753L0/9Fm840/9nN4SGAygT05gjo9YFajVHOqTjyDv3UXclhhlMeIgwGq5Ql1XiOOYU8DKouRcvpPjQ5xeO2ZRX2xL1HWDpmlQlBVWqzWW6zXarkFIIJIlgeaTX1cNtusSq&#43;UGy/mK78ttg7pp0TYN2kZzCFuFERM5UCH7G6YHA35svexQFRZpmuDweIA8DmCXFcyqYfTPFksSO68hY4DOZR89fvWgMPJm4IVwZuEOCzwPq&#43;B5&#43;gHogjPrgN83fD7fZyN&#43;X7XDxD8GlECyXiBfzaCLNcpqi6YBis0azSD1nTrA4C6OQgZ4gVIYDXOWAlVRs0qgC9Ik2usOdWugmhZBJDEqEsSxYqugKgp&#43;frGpGO1XRckMYYzgZFACiaNxhsFgiCBI0NZAvW3QWo2q0xikIaeYy7hDOIiQjhMONOb3tpgsCoiyQXSY42wywPLkFB0R3lhgs3Y&#43;g8dnGCkfM/gVwH7DWvEjIbgOobva1Ojp1zMxwK03&#43;0CPdTa/wBGsveXTuI73c/YfuvqkjnwAe3ICcf0UYSCBs7sYbJY4VgZNGuDu0qKua6xWG0wmY2RZAikThAQYYb2uD/lxYgRjHBAMVMT3miAG3RvNyaDz&#43;QppEsAawbp&#43;tdig2JToWsM5gkpIxHGCySTH6ekhTk4OkXH9oEJdGayCEk1lsa0sklQiiCMEiYAIFTMvK3FCwlWL8bZG1LVQdghMMyyPT9F5C0AQE1z1BzywQ3u&#43;lK/6opNPvG&#43;goS/&#43;rLGC5yUBCLQMreVT/9aevf/4NnRk35OZd3zExE/HOaLlDN3iHrK2xvhgiE0kWVw2dY3FYsmg7KWXTjDIY5RlhaLYomsbDPOMASARUHeagdpwOGCVWVYN63Qy7zarBgoNjg7GGA6GEJHE2m6gWw2jBVsPKgiZ4AfTCY6OphiPBpwq7vpJCIRxiKJssNpWSNIKRgm0tsVqAyTzAGmgGJgSpgmaFoNFzfUjRk5gWRJcdz4JAr7b7eNK0fq9zb1V8Ja14qdCcA3CrA8YPct6dgZwPTJi6wDLr3vxf/I41M&#43;LNjROYKcHwLUTxMMUU1sjazdYNwUitr8JlGl0XcM2&#43;GppcPfOGYJAoBpmjNgvzi7QNjXC0GX8tm2Lrm2Rk46eDtnWn8&#43;XuH3nLppFDW1bQEcYDgZ45fopm4&#43;2M2jLFlWtWXWEKuDKotEgxYhN0IjBIS0CocY0KMs1qnLDEdxQRtiWGtABrGgxSiJmzta0sLaBajXSZYsj&#43;j5Ssgt7fXziKpL6TCTzSEywLwVobz&#43;wFp8Igc2lm/jp11MzwM0&#43;zm9tH&#43;l7DbC/5hM98k8T/TaMuFwbp9eQHoxxGGicNFtG&#43;43VCNlxA2eTdy1010DrFucX56jqAkms0NQNtpvCOXeUYkcSmYWd1hiOMty4cYIsj3F2lqCuN6iKFfsrBnmOQZazT0CFAqNBjlkSo2tLBpBxpJBEAeIwZOJnBDa1QasNdFtjs15htT5nqVGWM4QqhK7JHojR6jW2UQJbkXWouYI8sAaqbZEugSMpoMUUOhthc3wCS6BQz1zY&#43;fGqwCfR0B6LH1iL20JYQpn6WdTAM0kAnyKtfJrTG96DNXlkIif29P4gZ9AXHh5gkoR4GR2mncWqa9gTKMOYN4SIbkwHFQh26HS6xWI5d1413w4uDEJ0ncF6VWKzKRi5R2NnoxOQGw4SHEyHWC3mbOIN85z1fFVUiMgElA5EEtMRM01GGSajAYSV2K4bmE6g0x3KusV8vsZ8Nsd2s2TTs21Kl2NgFJQIUbcJiiBGaGM2GZV1dWEhWRFNhXQhcBAolNEU9fQANaHKpoZYaucBfTgT9BbBxO/xGwJ4BxYrwOrPaFo&#43;dD0VA/y9v/dttvms4LvYA743vAn4eJufTn8Sw0wPXKsW4ofFAgoVmnqL1XzFLtk0iX0rGMOEzPMESeLMvqqu2LlC&#43;tqFSSQ26xJ3MGNHDamMtmtRlmQKOs&#43;esxCGaOsWeZohiWJ2GLGLnvMCiAHotEeMD44PD1BXHe58MmOPorYd2/JcIFIU/J7ELEkWsauYPrchs7Gr0akYkcoQqgSBiJx1LJyEYiZYrTDOI2xPxugOj6GLArauIYpPBYSx3&#43;M3LHAsgDvwZWdf//o/x9tv/69PTMunYgApdo0wlN1l&#43;djXL2vlH336e5PPTqcuv&#43;5iju3FXXzSlBBtidVixqdeyj5SLHFwMMHx0QGGoyGL&#43;OVyybpeyRBdQyd/i7Ikm33Ndr2UBlVVYbvdsv&#43;fCJ0kCYv9rSnYYhgOciYg2f9NrfkEByqGUgFLlDxPobsKm/US26KECoHBKEWex2jaBK2uMRjmuH7tCEGoMDtf4PxsxYxgud4Q0NYgJKtARowt6LDCdJBlgcFsiUkaoRkmKKeHHE4mW/cRzSmwJwWmztEmXvFt7Eq2dh&#43;NIR67nooBxGW3xEAIHFjLp//lT0X&#43;HPlL2L9vswxyW0DeuYf64hzndQHdVixSsyzk8i3rv9RgkOH69Ws4OJiiLJwHsK4qKBWy14/MucWi4BNIOy&#43;VQVGUfCPTkFA9nVIiuO5aVGXJuIJkx2q15RiCEIrVBX23pulYpZCZKVXAXylNI5ycTBHHETODRovRKMfRyYQ9ibQd222DqnLOJroOY1w/AaHc4e3TflXXItqsMboIUYWH6PIhmvEE2Bacz8iM8ogd3EurewOCy9AXriHFp5csPWw9MQN87dY/g9kJAETWMjp9w6uBRyN/f/rNaMhVOlzJu1hALhawRcluWyKK0RrGBGhq93OxLZGkMZ&#43;qKAwQjYZsktUkMiHYaVOXLXvuiraB0R0j9mJboCgqjMfO3CNTkSwKEt93tWsCEQYRttuagz7TgxxRGPmuo0BVtwjDmL2N9HNGwHEwgAwkkjRBFMdcQcwRw5YwSoA4iTjsz&#43;XmVwpJ6ecGoQxhRYDACMjaIlmuMEhjbA&#43;HaCcHXgrUEM1DI4X9noe9yrXAiXDpZA19xM2b/xTvvvt/PBE9n5gB7I6&#43;HPLNnb/avuw9Vo/2&#43;rHNn8EcHLIKUEUBtVxClmQHt2wT9/FksqFnFyv2eG62Wz6Nq&#43;UG0/GIiUDim5iBQFgURJhOaywXa/YJaF27mv5SYLOuoI8Nq5O2aVl9VFWLqmhYVMdRjDzPcHw0ZTdynqYsYRbzNdrGcHkASQWSBGEYsSSx0K5K2Bq0XcfqwfY1JHHEASR6b3f0jesjpV0Mh9sRyxBGSCgCfKVFtIwQD2Nsh0Pog0OWirv09UfTbMR7bsWpBTIXKhZP1f3/yVWAy57dz2w99dG/5NN1/5B1vyDza7NxffnaFtZo3tC&#43;O0PbaCyXW5YABOiUDDhVi0SzFBIylKzrrTKM4gng5VmE&#43;UzD6AZSKN6/zabFvbsL5&#43;vfrLFcbqA76zyFVYcoTBgYHhG&#43;GOYYpBnyOEWz7TglrJadoyNhXeZNZ3V0bcsFo3Su66bhgBJdI0mAIKRTXrJk4BcZvds4azUzgRaKv4doOmAjEWwSqMExuukUliRiWdyfTn5lN/1eH/m9H1qLuWBFKJ5YDTydBHB0Cj3oO90r4Hz4IsUaxZzijTRjn3y42ULVNW9QT/weWtDvXePasRg6aU3HET&#43;6J/FOGy7Jnja9G1ixjg4DiQou/h&#43;ECbd4n83WWMznrPe3m4qDOvz3IGbiHx5OOUGEpIXUEqkKIY1EualQlZ0PCGlstxXm8w3nEqzWJcqiYW/esNWY5DlLiZrwiLDscib60Ul3V&#43;jD0LKFViECGbDlAtvCFAZYRpCTHEhojwbAYu6SRh6fMzDyFsHE9ypsniZv8CkkgNjv0Hncc&#43;GVvnz3LxL/ScI&#43;f3aXrAtEBLy05eZNviKf/0mumZcM2mwg0XVi57MnAtaTFlnmNpAIoDtnBpPZmMQRmihEluWYHIyRpDkDS0L6hC/olMqImCdgNXJ0dITJeMQiuy4bpEHEFk5VNthsKrTGOh1vDDt16CalxWZdo2kt5xWQ9XF46FQEO4u6jsEeWQAs2YRgZtD0z7qwsJLWSUVtYU2Nbm6BYQKcnLoimCThLONHeAfvl77HO2tgv1nli2CAX/u1/x4EcK3rsp159&#43;S1K0meD1yu4Hx&#43;m6aQSYaoMUjWNcKaToVCR/Yx38D5gIGyfJpHo4yB4mZTcjSPTDX6uW01EzAM4dy&#43;xpldURQiTVN27R4fH&#43;DGq9dZv5MlsJjF2Gy2fJK7xsJ0EsNhhsODKbuEjd7y5YtAYbEuMN8W2NaNkzBcpyKZMYkeZPLFaYqwrNgfQdZHnKw44WQ2X3EjqpCkHYNaxwxGKk6U6IxFJy0DSa5aMh1s16Jd1jDnoWtcGUfusCh1f&#43;Lo/uqtgWueBrRZS&#43;K7b3zjf8Df/u3/&#43;WIYwHIyku0vIPN6aPpAMed9DMBZvmkGGUSIS4200JCaiB8AMnCp3tadijAKcXw0wY0bpxzuPT9f4O7dOcf2N&#43;stS4EsJRPO8MklbNA2rncPmWNJEnNI&#43;OR4gvF4yI4bwgjsIm471EWL1bLmPkGDPMZomHDGNul0&#43;owLwgpVgZZUSaCcsyeJMBimGI1TJGnAeQVFVWJbllgsC9TNHfY3NHXN6WVJlmCztqhXFecaBirw4tGyCpFQbBqSxWF0xVHIdhbCHhzBTo59aXv4ODXQl5dNPQ0yJ5XFft&#43;FF8AAxrXi22OAqW/Z&#43;uj38eIfgyFCBEiKAnGl6VgAImCzyErfrElYJHGAg&#43;kYJ8fTXbsXYo67d86xXq1x5/Y9jgEoYdljWFUVm3hkFraNM/XoRiKYrjKKQ3bYZFnKWcJt1SAJ5uiaErHYYhCHGCYdE/XudgnTLRFGGmGUIElSNvlIkkymQ4wnGVf3G6lZEtB10bWUZckAL44DHJ1M&#43;bOqpkBnGlYJnQ7Z9cwQUmpIkiyyhtWEcxp06KBXAphfAKMDiJykZfJpQSI6OUNAEBOk1pKQssKaJwOCT8QA5jLx02MAO/JpzI9s4sDlXGkKkeUIrERctoha40a7iBCQeldHS/sSKsV&#43;ee7r19YseqfjAdarFeazFT7&#43;2W3Mz2ccKeSKb23ZuUNSgJhASmBbVFgt14hCyVnCkoNLQBZJhJnFEDXazQWm6gLjLkEUdUijDaLhFtl1g81hDoQZVJTCihRBmGM4HiDNEuds4tCwZXOQQKr2JixJr3yQcnMpOoitadF0NZTooEjaCcI4HaTunOloHbN2toMtLOR6AdFWEEnqmlKo9aPUgLgsKCEaiNSn48HoJ0vyekIQuN&#43;50yb96JXHBn64yJM2M0HUAHFjEPJ3chO5iAlILAraSF&#43;kSWifRDaJZFpJkiDPMmaA7bbg4o0wEGzu0QGh55Od32nNNRjr1Raz8zki2WI6ALJQIw0txnGAUaIRxGfA9mOEtkRqgNh2kHmNNutQnYZo1BG6dEqoCqtSoDUKYRywDq/q1jmtONJnWcJIFTPgywcZsjxj07Chk0/WDEkkAWhhHcwlM5YbUxmfpEo/aaCVQLWFbCrYbOiqo0g3PV4N&#43;L7F3JVMXlroL4wBHFndiRexB4KPzvrpW6zkOev/aNvx6Q/p5DLDBr6bqgaYCVxWcFXUnJ5FppUKAoRRhDQfIooXUKph3Z2lIaN60p&#43;EAzbWMmgjvVpt1yjnLRArjJMO13ODUao40JPHBnEwhwhmMHUFCYNYaiRJBxUZWBXCpBn0uEMdCmxahdpI1MZisdZo2hZKtW54VCiRZRkGw4zVD6kKYtb1tkBV1xxBdMnlzioAFwoFrIq4OxlhH9ee1JWcN52rRJaSS99kFDk18Gg6RF4Vewz2QGOKT11PGw72ZUwi8SnBj5YAkQOASoWI2wZxZxH4MC6ZgKQb3TwHQsoB68WqcN660bUBhqMBV&#43;rG8ZqTLkMVcyfQ4SBx4eFGsxfQNaDoOKQ6iUq8nC/w5qTBr04bHA&#43;BkMFmCFkJBKggUEJLV8UbSo1IaoTEhIqkVgcRdzADiwMFtGQa2hbjjeDg2&#43;y8wyC3WG8UE//o6IATQAi0khojy4MYwPUVFG4IiWsP6C1o6SQAJ6q53/lfq33msM&#43;UiuJP60HUt7CPWZz2ufBPsJ5BAtCH2ujRsX9XIWt9c4ZASBb/UWeY23WPir0DqGcE0qldbbFZ1RiPWgwHhs29uiwY&#43;A3yxHn&#43;8oQ/RYctm1Nd0yCwJXLZ4c3TAm&#43;9XuLm9Rono45Lw7tGoa4CaC1hCYhJs9cMxJWKiV3zBw1hWijbIpQdoqBBKMCfb4632Lxc4ux2g7a2iHNnJZBkKKsG6&#43;2WawgYK1jXWoYtJ2J6wSF0Bru91bOjGz2l67icjPnE9zWgPXwEC/R0iLwEUE9&#43;/p/YDLzS4yfyr3&#43;E&#43;YddXx0RhhwAiVuNkCtu/N/9VBbh3UHSfyfTEQNUuPPxOad9NW2D2fkFg0IMot0kUPhRcWQ5HI4jHIYCL2UtvnGjwq2XKxyPOyShO3kkwjvSxUQUY9ljt&#43;tHIfaniroiFRefaCF065jCVEBZI&#43;mWeHl0D19/fQ4y7rbacMunpulw72yG7XbN4WpiALNn3grPCHzF1p1&#43;9gYKbyB6BuD0Z&#43;ubUwXhruXMpwSHIpLE9ilqiJ9QAvRnxvY98x8t/tHX/AeQUiEwBkGn&#43;XS5mUxe9/vf7W7YhvuyZK7N2hrLxZJBEhFfSMsuYrohjVxv3yCEDC3C0GAyrvDasMJXTlocTiySXLHb2HYWiqzCBpDdfd9I2N0e83IZIkx8mA5W17BNia6oUM6WKNczxHqGXznaIg1S3CuOcLbt8JHpsF5vseCahdJ3FffAD647ifGEt5f9oLz88&#43;1Mub2Nr4kME25ZB/lYkvbzEfuD&#43;MTBgCdigEtGZE/go9O&#43;3LPdxSsn4lTn6vstmTxwQzqF738g9rpryf0TQZsowDH9ZJx7n7phfBBHIRCHCJVFogqcDBa4ES9xfVBhMjaIhhJqqLi7B32cCi0UndbWZQBdOVC7Lm3&#43;QdO5NjDtBpbs9LJAs1iiPl&#43;i2myAoMAwa/HGicG4XUHcHeOHYeMykrVhC0eSdSN8Y0nhm0bYy07j1reosV5rC9970PmN3eAR23coe/ySV9Xwi7cCPtsS2LV24zYs2rD5406&#43;YSTcqzG3Ca57qmLBKNhJJAKLfBTh5NoIk2nOaV537s3YaUNWQhQrDOIOo8kcp8d38Mp4gcmwRjo1CCeAyi1HHq0JYSIF2WnYomM/vvVWg1KWI7ZCw&#43;l5Ama6gazmsNKye1qTXm4qKNSQaNHUhr9aNmoQJBdYbAMME4koNJxRZK6MIrSeoYnkiiWO9T2JSRmS&#43;Stx2V1s1wgD9rI93aNVwP07/sTrCTHAlZ/MpyNOJwXIQFXGQPpJHBzwsVzD6xo1WQEt4Pv5SK6wlVIgSiSOD4c4PZkgHyZs55dly1k8m/UWatMhGW0xObzAS8MZjiYl0qFGONbs8BFsoCoYEaOLElShwlwLLFYWRWO5qpjUyiCzGGXAKHUxmTRuEGGOQG6BSELFEnGooFPJwaZ2JlDXGkEDJHGBg/QujocpBpnEchPCQnkGcIEkVm/GMtZwetrFCHZVvnvdR513y1wOLfj0hE&#43;7O1W7Xz/7eloJYHx5kn7sJ&#43;4ahbhKGWHt7t4y9FP8vzOK6HQEICtZCoVACuRJhOlwiCSOOY5PMDFJUoxGI64DsO0CB2mBV8ZrHA9L5GmHINFQsYZQlnMGmk5jVRqcryw&#43;uR3jw481bt/RmG8NitaJ/yy2mObAtaHA9bHFtbHB0aTCNK6QRgJhQlgigTQpuihCbSLURQARp4hyiQOh8fI14GiqMFspjvj1I2aEcVjACOcPkGwGuBAxO4Vk6CWA2mGBS&#43;nxqbi&#43;H1bRfbYD&#43;eB6MgYQPrvJtTRrnOGNz5CN6K5L&#43;q7QPKQdDhX3NgCY9CQkA94kQtjDKMUwzbmPT61bF0pVAVfs2EnCqWSvjFqcjht28JCZ1oN4siSqDrgzt/jxJwY/vt3iJ7ctPr5jMV9alK2zP8C&#43;CIs4MBglFicDgVcOLH71ZYtbocGNocFQCYRBx86fwVii0WOE1THSyXWkoxCyXuHV9RqvfFzh9qxD3bkxM8ZoCI76uS3jXTD&#43;VBvhBaQLbYv7T/re7PvHrP4gNv1omhdsBXi5tZu2LbzNslPoV5ezedydcLqd&#43;F1a1rRsDwc9&#43;vcqQblYGaSVSBAhCSJu7oBWYL0tOQ0sTWOE0nLA7OVJi2neIVSGr6QtScBKdDVwdwN8732J7/1Y4Sd3Bc6XFpvCcmOIJI4wzBIONlVNi0VZ46Jo8dHC4P0Z8OEGWAnnbX890sil4Th&#43;FIRIBocQ2SHi4SmCGEjDENdfTvHaa2t88MkG66KD9VVGfJqlaxnHRNUd1wswMGRcIHdzA1z7WnHZ83AfmD647OVkU245q8VTNJN6ypQw/uDalyY9ulLVurQolnpK8mkIScOzSOzb5ngGYaKzwcSMwrigFWgrIDOKmaYsOrSN5cqdUHQYxzWOBg3ySPO&#43;We7vL7DcKJy1Au&#43;eA3/9foCfnsVcsKG4e7xGaC2mwww3TqcYDRIUVYt7F2vcm62w2pS4vepQ0qkNXM&#43;qOLJ45dgiCjV01/HsIW2NC0J1BWAXGOURXr0xwelPQtydb9BqV&#43;BivbrrGUBw/MKJf9uni3mfCaeO&#43;9F2PSB8DPize3TgGQQPP4WPX0&#43;DAezeyLTy/mGJl89ywAdaO4s/kOzVkuzwVX3PJK/ELLOv4UYKDhpZI2CKDuf3ClTasApYrzuoSLGaUFpjENcYJy2jbwJY9HGmE5jNBP7ursB3bwt8vEqQ5wd443DKUcZtWTPB4yjA6dEI1w5HnGA6XxV4/6NzvP/RGRbrEp21&#43;PCsw3d&#43;WCONLZubk1xyRfByWcIGK0hFpkYB0Z0jilJcO34JN149wIe3gaJcw1jhTzl2&#43;tMFhIwTBrvt9JYCMQEzQOj8IZxX9kgmsH7v94ZQ7FqevhgGkMLJf7Kg/Idu&#43;zLlh76AjmSf16ck2kAykXeWv&#43;gbJ4qde0R74MhvWGvU90qs1i2MNCw1htMYyihOmkxkiyTUTvfv4aVNBXxwV&#43;LDMwWZ5HjtpRPcev0UeUqSoENRNey5C0OF6SjDeJDhpeMxRnmCQEncPl/yya3rEu/fscgjIJUWL40V6spyQ4p0sECaHyDMA5fKZktMBwYvXx/j4LDF2UWDtvPDyF3w&#43;xIusax21o7tPazisj&#43;yCEInzLllyWMlQE8DP8DaOxafYD0RAygl0La2/yb9nP1yD4FelUDs2XJl0m1oUYVApayLW/lsW/dmjqNYCuxUguNpVbmQapwGSLKYT39daMY8amgQSMPiv/cs08ndtgKbNoAMMxxNx7h&#43;NMbhZIChJ7CxllPL6D70c4HA&#43;y8YvE2GGWcY3T5f4Pa9Fj/6yGCiLKoDhYSktKgRygXnFMTJEVQygQpbZDLCwUGK8SjlApKq6nxrOved5c5x6qWcR/6GPaHG1UwSsPGdUdF2wMM7idj7aFD100qDJ&#43;z58mQSwM268EebRc&#43;cDpzHAdHVS7TwwXrYpkYbZihCwTc0lq0B4cPX2kcEnI/QZQb1mIC&#43;aRwoDCcpkjxEWXechzeMOuCg9&#43;HbyxHfWqDoJIIkxavDI5yeHPHJX25KJsbBJGfzUqTicliUcIQnRiBmGQ9SNG3HDL9YbVhinM8NTkKJJAdnDIl2g3Z9B10mkOZTxMMprBhjOJAY5RZJLLBhp9BlvQP7RNBHA4WPR7gL52hI4DqbMyCsm8tuog9XAZ3be7HYO4SfwXH4LAwgvUfTctPCwk3GEnPA1j4z6IoEIPEu6ga2LNGlQ1SRQsFVvpZLpuXOHHQqoPP81UcGVD/3N1LIRjGyUYjyrOHmDBE0tHE5cHwzLmmTDk3nS8Cn105w7fgAVdPho7tzJjzp/iyJXN9Af53aGM4JnC8LjupNhin/nZhgtlhjOWshRcWSJlQCgSRVS4x9DtQtlKkRyOuISQIkGi8fzPDhtEJRSGyKwNcIWMcIwtv/tJHeR8BdzIXkWUbcqp4eK0vXUu7hKsDuja3tm0cxrn7SQuEnYoDvfe9PuBUsD&#43;ayvpEhcM9Lg9EDkUFCu00DUWxhxh2qUKIgCSc6hET6Pk/OM4H2gLDzZ5oTx0gaBBZRDowODFpboem2iIQrzGg7ia6TfJpakpotXZzkuv6DcY7pOMf5fMNTv8JA91Odr3AqmZarbcVM0nUGr78ccnOIV65NUJQlbssaGRuu3V6tB71fi0RVCPQGspghVDmOww5ff6XDcjbAthyj7SJUtc96ZgdPt9smN1jYcB0C&#43;/1j1yWdiC6L8jIy&#43;ODyEljc8zQohJ899Dd/82R9Ap7FE1j52Xh3vBrQD&#43;QGWM7v4v54pm3QBAplQEStEQjL&#43;jzgeXG&#43;4sbD2s6rACIVoe8oazE5KvHK6xYnJ3dx7eAO9HaNQVJBa4GyUuw76DTQNQKBVYiVc9&#43;mcchAr&#43;00B5Do9Mv7jon14WX23nEDKsGvpde9fDyG2c4QFRsoobmEn1PXhEWcdIgSDWUr2GrFUc&#43;RAr5ymmC&#43;AT6&#43;yHGxjNC0gqUVu/T9azkBRlonudhDptiziChxHcjL4lGVwr35t/Z7f345i/DJ60OfkgH4gxqPAe74ubkPwQFw07iJAcoCTZqjiCzHZUNoBIjZMRT4CKD2b&#43;KHt3GuYJQ1ODrpcOPVAl95XSMwH6K89iGqZQlVtAibFm0r0RkXcOFypcRVS2ahQRxIROOccYCUElkSMtjbX0pKDLIEL1&#43;bsNeOMACHmsnqSBUOcsmBonQ3K8oiig3i1CAIHcizpmPfhZICw0zi&#43;FBjMtJsovbzcuzerADn&#43;ZNu4BRzOuGS1FkBde0KQ7iL2EMJ0M8qvuNp8FRVQXg6BrDObyestpYv4rbnwofgAAcERVXCbjdokwRlogAiTE1MILh8qvP2gNkDguDZTRrjyQqvvXyBG9eAw6FFaD6BVnfR5RX0BugWAnrNWZesLoahxSuhRi5aqKRGrDREGCGJQu96FQ&#43;4XYkhBlnMFgGtKFRujKzWSGWLo6xjvZ&#43;Q1OKTb5APNdLMMDA2RviGGc6zt0P61kNz79u/jA72FpAzh4QUMEnKtZOMB7YFRFm5&#43;sAHOaDX/xeeAVbCVQQ9cq7tc2aAS9JCoIDli/jEz7wZPgwHoK6B7Qb6YIo6iyGSACi2Lu&#43;Ct8Jw0gSZRRzW8vV7Kuowna5w/ehjjOMCQUsYagmFAipueZ87I6EtJ&#43;65xhUSSKzFWBao4yVajFnWGBldRtzuW1yqriRUEl0maugOut4iaJcYqIKvhTMvlEU2MBjQ6U4Mf92OmM/60DJJsI7An8B6K1k6XRZrXJ5&#43;288epH&#43;hBEZDdyPeL7auZ9DDawK022vxMR0&#43;P430qfuGPjEDvPPun7phz8JzosA9WPEBYM99nVpwPw5wM/lKnrnTZgnEIAVWF9BNAWk3/tQErk5ABBAyRiBiTt8JoxpJtEZgZuxft3DFH0xrAoeZcaCsFq4am5A6LGJRoBJn2HYJp3cbOYKVbrSLwINhVud/ctm6um3Rlls063N06wvIpuTsYdLZKrBIUoMkM5yFxD0IbT/0xKV/FZXE&#43;UxisZJoGKCKXXt622cg&#43;TQOGwmA9uR4AgxyiG3Jp/8RTSStV72E/D8QggF43XPWO08xlv7pJIDLYGEvroW98MWJH/vRr8kDfQKIk6uKO4KbUY56nKKb1VDb2xBN5YYsWQnBWTQJVDBEHE5R1DGWhcVyq7iFWxxUjAuMrysIpEEQWQSpdnUGrQuHKIaQDRK9gDaSm01U8hBajSDCBFKFbl4g9jxI1nBzia4uURcbVKsl2vUFRL1EQHuuLKTz0SCMjLuFhoNZgba7SXSNFZhtQnx0FmK&#43;kS75NXTRQKE6iMhAxBYiNUBGtwBtnqDLJxAmhDyfuYbSDz/9vf/lY98ubiaE8EbT54YBwMEM5eboagFsLF&#43;Q&#43;BCwX39oe1g/llWs17CHE&#43;hBjI6UxfIctjiDqSpO03JMECFQA0TRMYw9wId3Qvzo4wTHkxRx2CCJDOtcl5kGt6H0e&#43;uTinbxFTKvKshuBtO0qLsNajGBiIcI4tQNiOKMXP4aXE/QNRXaYo1qs0Kz2fDwCWUaV3IRiJ3Q4IGhxBDSckKJUobFvIFCXQc420a4uw1QBgZyWnNbeZG0EGkHQbhhaAC6DQAdZTAihagy4HbnJqTUDxX/PfpfuL3Gx&#43;LS&#43;mJr4mnWUzHAD3/8v&#43;Ort/64z29rhMCZtTzp6p6vWL2aLczWQAu5WfMwJzOJYcZDmAsJvdzAdGvYtq9slmi43do52naKD&#43;QBJoMER&#43;OY26kcjw0DMtr0INAcotWhQBf47CPjHFDsFtaGm0U3xdq1kem2sEEKlZCUccMmHVE15/7ptoJuSui64iKNvkkDSZzOChbbZM4Z4XwNLq/feP&#43;74GKXLQLMlEQx7aDElg44xMBA5IZPvSXckFjYhK47hLER7HYIW0jI5coNu3pwmgj2wB&#43;J/58Kd9/Ah5je/dHTjaB/ahBIothqaX1SCJki79OF7akBeeXatWuLKi/mkMkRTHYEOzyGnX0MW6zhnIu&#43;qZJtYUwNYzY4m63wg58cuVawUQTxeoOTSYM40AgCl0pipECjJKySnH2siGBaoGgkNpVAWVl0VcO9fKwk&#43;1rBEOAkicNmna9Ksh2U1QiEgQ193nLXZ&#43;l4B7wQ6KxCoxWnuPfjZ5xyllgECvOxhf6VGkmrObprYgtLtwAuz1CCJ5FbE6GrJ&#43;iqAXBeQZ5d8B6Jh7eH0d70&#43;4nf6zl68f8M66nbxb/00tegu11VmPHlYhM//Wr6sJKxXduTKHYTQOl06DWkrXYh0sviFmdba&#43;Nm9xe14EybMOwwzDvkqeGCT3o2WQJVF3ANH4lirQWqVmBdKb6VlWSnGl1lwIkdroII2vUNDkWDgMxGodlBFUhn7wtvynFDRNL9KZAMgSgXXNXucl4dqNNCYGFD/MgmeE8mOEskahLxAwuTWS6hIaZi4EdmHyK0&#43;hD19gaau0PgJ0vI22dsAkI/IP6N9/e/D4g/B/CfhCDzmyeOPtP4mKdmgLt3/5b71XvS9rGYyFsC1/Zaxl31DHLIKgCGY2AyRJACQdQwmJOh5DRuwckjwr2aw8AaDTQKdNCRQToG8qFAHLpyGDrJdCIbHaCl02kkiibAug5R1AG7Yjvt0LgKBaTy8QPIvnTBE1Pwzfml&#43;st2vweJI34yAsIEfmK4YCxAGGELhQ9tgrd1jvdNjLlUaKQAF&#43;v2eNNFnkDQsdMjVNUrqOenMB8ZiA/PIOcL5/27eqj7vL97frjkvyfAL4TY9Obfs8wMeF5p4b14&#43;hEgvucnhox9fOCqSdi2EOsN5KrgWnicvgmVNhAjCbuZQxcVdNFAlw0sN4UyXH4VTSXaI4VP8ghv2wBBBzS6w5HsOLLYyhCljFCLAJ2QqIIQW7oJgdInMBBThT7bSu9qE42LS8AnrPblaiSRuJrEcoKpTC2DN9LdrXAzitAJyNaiUQL3ZID3dIYPugRzG7Bza5ej0KsPz3Rap2iaE9SbU3TnEXD3DGK5fpzrd&#43;vUq/gbgmDe5/LMncLxrAzwzrv/qh8OTYersc4p9DaAm3uTQa/2DiSLoCgZC9hhDlw7hDj8CoKogsglu4x13fF4V7Sk5y2ySYDRSYzBUQIzjfFxHsG2EhcbjeuiQ6YtOjrxZYSiClE1Aeo2QtlEKBqgagxnFZMYl9odRY4kGofmA&#43;V0&#43;f7NqYMOoeoQKYOEbtYgqg0i7YpJs7DBADX7Lj&#43;QId41A9wxCSpviMK757T3CDr1FKJpx6i319DOBrC3t5BnM96TRyD/1nv9fgDg7yBwV&#43;wl4TzrSNnnWBgi/NRL5tS/A&#43;yveiwwuaJqvBSQqxVw7wImOwUOrkFMtlChgYxXELXm1C7n3hVAHqCLIzQywqaJUc0inM8U3jcCI22RdoKLP0ncl7VC00oW&#43;W2n0BEA5Pfzncgc0PBhZPhWspcp&#43;L5ek1VCIDVC1SIMNeLIIA4Nu6eToEMadMijFoOshUkNPgoCvC9jLJRisa&#43;V6wekpeZ0F64RaAW6doC6PkGzHMN8UkN8cg65WLpxcg&#43;efuMDbW5PBYPs7bN4/u5fz8wA90kB4lYyCd/e6x4aP9BEyjr3sFiuIGZDmHwCnb4KERFvz9zgRWInLWEI4LUK3VahvOvm/8FIruYhLBd0FqqzzDBt1w9&#43;cqZZoFxVEv&#43;ttSy2SccHgWJGcGVcbpag8plCnMHDuIDrmVwDb0PYQHN8gN4zCAKEgUUcCUSxgE0k1pHCJlTofK2uiSzfEBmOfXCfMGJIfYC6vQ59EUJ8MoOcLZyr/OHEL/1g6e&#43;RZBUu5tL2JYDPY6D0c5EA0koufPBSYCMEfmyt&#43;CvAHvv4wEsPtpEVLt&#43;tqICLCjoOgHoCsRWwmwqoWoiW&#43;6ux46f1gSJBVqfx3kVtXcKET0DtTTWO7g1SHB25MvKu7bBaFSiKmrN&#43;xuOMYw3bbeV6/Y1yHjBR1y02W5f4kaQRM8x8vsZiWbM/wRVzuCwkIT1YVYpEBU8NMcplErP9EwrYWLkQYqpgQ8nxCB0eQGMALBqIbcW9gR&#43;Sx9mLfgJ&#43;/xkQf0V76qUBn/6n7A39wHouDPD99/7lTgr4Cz9nfQVx7JmALIKDK58nfcbsZuu6YFjN9XdoapcI0fo5gravRBO7NHL0n9SjK1z&#43;TCdbRgLjQYI3bhxhPMnRth13Hl2tSmaIw8MhS4LVsuB&#43;QkeHYxwdj/g58/nGtafPEs4IupOSrd5iVjWMIy4/0LhInqee9apld3M6BAgD2DAA2aw2ErDpFiLi5EVn7gWK/Rf3dfjrQfV7RHy3l4wDdqf/vR9&#43;2SaHOqu5NwlJpn0C4LuAOAXsgVcFgx4PcD/csvBDll32sCO4n6p1dcz6E10JT/2IQ27dqjuXJ3F4MMRknHPjaRf2tSzOO25O7RxKWmvuNjqd5KwCwlbi6HCIi/MVlssCGubq5Vj4XOb7Vu8aYWZofdGH5BRyFW53iZ99xtR9x1l7Pf8TsvfdHuI2LOodtz2b7&#43;fKem7Doy8uvrPnF&#43;CTqMky8Fc79BIg30UL/bBINgu7zjdIfmwe/GdaHNoNFJ/88Shjsd62GmkWc0VRnsc7e58YhU583bScAErinlQCMQtjAOlQ4Xy2YenQdRpXj/mnLLtXH2Gsy/Th4dGNGyFPOIh&#43;v/zOfabVzwD8B0D8WwF8XzhpoPtw9vPQ/f16ruXhdGE33/w2MerORSwEvm&#43;tGPt&#43;gqHHAwmH756A0MLXC9xfKb0robvvrYx2lblB6AI&#43;xARd50Agt6H1jzV1yzGDDff/rXlOAYn&#43;SASOdtru8gh3xR1i/zrsA5/90GrufYlmjA9JX/lbT3ySnN/xHr/vezd715cPvPven3zmPfss67n3B2g7iyjYpb43PmvlO76hVN9W5vRTR8vsrf5UR1HgmkRr14iBkHvfSJKIaI3L66PnkN4n0DccphwpXS433Fl0mUR8urltTN1isXCd1rnRtJ/2cef2nC0DerypO25D13&#43;WG1jprQZjmIl2TR/20r1cjqF5fGWX3f&#43;F9&#43;qeV5v/1nn9OOFzZ/M7CfR813NTAf2az7&#43;Lo8PfxM4PcmnOFP7Bkb8ljx0u5RdtJhF&#43;Oh3g2rUpA7gkifgx&#43;vna6ZQnhJHOz9KI/0Y3EvVEaCJUyCPlNU8GYcbhQQ8W63XBTaEDbhub&#43;gHTrkG0ZHPPTRclBoDv6EFq5PhkjOOjMQNKsibCvlt5GHC30CRxqZFaW3yGQR59lI&#43;A898A4v8C8JcAPtzP9yfG&#43;&#43;GP/rfnRKXL9UI6hLzz3p/uzRRmLFBYa&#43;kL/ZWXBMLPwDt&#43;bJ9huEbNZNJdv36I09MpE5ROc1k2OD6e8PzezabEYr5hgnHQXJsdMYkwYwJ/lRswHcUhTq9NeUPvhAFLDCLqAel9JVlqECYYjXJuWL1elwwkyTIgZiFGOzoa82tIyhCGqKqGie5ay7suoAQc6e&#43;fYq6ZvRDv3wLi3wH4j0x8IQruaeUZ6J13//T5Esmv5y4B&#43;nV&#43;8d19UGiFEJ0QzNFrX84ce3D4WEnA9Xt0&#43;k8nvPluufk/x8djJm7btEzIo6MRS4TDgxHb&#43;gTiOBU8T1hUE9PQqaXn0L3xc32ISVgteFOuYP&#43;AmztApiITmKRLHDJTRHHAEoWYiR7P0piZ8&#43;BwxE2leRjVpnTjaB7NAT3x73qxTyf/z8neF0Ks&#43;9pRkjrvvvdiiI8X2iOo9xK&#43;&#43;cfYpd0ASyHwQ8u1H6JvgvuWb3n&#43;QCqZc82S/peubq/VzBBZFjOgU3ziXa8f7VuxkGnH/QQsmHDa&#43;H5AWrsKHY8nAj90grFFHDAwFN4yIClAJ79pnP/AWINpmPvxMW4sbFE2zL3EBDypTLnBUnkW7zDLY5b2gO&#43;e8/Ix8f9CCE6tW&#43;&#43;yfBj0PT/E/7D1QhmAlmk6yJg/pu8ssgbs&#43;/5348vKft1bB4N9aWB9u5SG0HrT8bg36XP5yH4nQtEJY7Fdt&#43;zto&#43;c5jODm&#43;7I1oA2fRnpOVdasMpSUPEiaxHQQSu48Qovel96Dx9MoibKs&#43;XECiMQwdE1hFHKJGXEvfXZZuvcmZiAMQNYESQhrH7AQ&#43;rSujc/r&#43;88A/syZfEx8n&#43;LlXlU3zx/03b9eOAO895N/ja999Z9zJu8uti2whuWslhYQS8DOAPyOjx&#43;M95NJSJ&#43;TOL24WHlmiNwJLBwhe1cvP29bYbnY8qmP904mndqyaFgikFT5&#43;ONzvhI65aVnCFe/56J2RVGxVeFGyDlHDpuIngGGSnF9H72eJASpFuXbl3O7WAaY5W7esF&#43;9l3TpM3r&#43;GhB/4YAffuZnAWsHNi3CoMQ77/6bF02epywnecp16&#43;a3cdkThGOmiRf/XwPw&#43;4D9JoA3/RCE2LUkEII2lcQ&#43;EZvs&#43;o4lQstAj8DaeJKxbiZC08YzWIs8GhfCjYDZuqbL9Lwsi/hEE3Cs6pY3gRiKmIjAXW&#43;is7XQOUBJ4j3110BqYrHc4t7dJeaLDTOk8AWTPueDJYg3EfdLuS/23Lt/6e38e3SJl/H95&#43;vo&#43;bT1wiXA/lLSVfT6pb2Zc9tvzgoQF14afM37CgbW2qjrtNhsSkFE6p0sPIZVSd5kMvkm45wBGIE&#43;Ev&#43;E2ulnPo3SPY90PT2PCEoYgE6ucZMWHF7QYAJPJgP&#43;HIchNFshUegaQ08mOTPY2dmSVc52UzG&#43;wC6VzS1XCGR7&#43;37jv&#43;cPPPH/k0ue6cu6Lv3JQr54sX&#43;FJp/nh52df4&#43;tg&#43;Oj3/KtUjiGov0JWLoTIubeF6732qAqrkl3A5yEZi/fpY1NBCUiKaX4JHNz1kDx6aaVDxKWCKSjefZw5Ua9kcQg0EgM07Qdt6c7PT3AjVePmakILxBZh6NsN6AqCAL2H9y&#43;Pdud/v5a9qvALuv3mfBvuxPPZh4xwI9IiAgX5LR9Cxky9c7Pv/d5kuTzZYB&#43;ORPxN/cfsj5usHY2sbjjxaWvevUtRa/mGHqg6Mw1IiLpcTqV/WMk&#43;tl5k8XuQ&#43;gNy4Z7AQRc/&#43;fcD4wXrGCr4vBwhMOjMeOHxWLDoJD&#43;TviCTj4Re7kocPfugh/bM/PsXv7e1iP8H3q7/v8GxP/jbP1&#43;9r/Ql619Ld59QXb&#43;p60vhAHgmeDg4Dd2HjZXcMppzoUQXPFyz9W/i5mvhjGeAeQ&#43;I/QYgfQ9EX6zLvmebH&#43;y28kmJ7zA0TxjWQ0QQekziYBtp9kLmLCLWEEq5/0j0McEZjAoWNSTyqCPLQhsLreMI4yx9xOeGPfH3pX7Z4D4MyHEdzzKX3ARmxCuNYAPer733vP38H3W9bmCwEctn0vQL&#43;E9hZGfinkNwFcAfB2wt7yl0KeeJ1KKME1jOZ0OZBSFglA9qYRr1yY8v4fEdp8LQESeHgy4KxipitnMDZo4PnZOppKlQ8v1gXTSu65jqUJqgsxIwhFJGqGpWzufb0xZ1sYY23pJNfOBHCL0O4AggPdjIdjRs7n06V&#43;mf3yeYO9R60vBALS&#43;&#43;c3/CdvNls1FH2UTvsNs7GMHZC3cAPArgL3pmeKaEGKilBzGcZhIKQkwKimFyPNExEko6AT37t0wCFi3k8Qgu70onFlI&#43;IHwAOGDuu5Y7PdJKD7LxHattkRtFShttGnqui21Nhtr7cIHvD4AxLse3H3kVcBKCPb2&#43;Tpvt91fBsL360vDAP3aiyHw/9bu5hSH4EHJOLSWJACnnt&#43;AEC8J4LqUgiQF/Y0nmUkpIimFIjhgjJH0PpwtJKXvwegih8IPhuwjeJd1/LtAlhsEavkEl0JwSvaFMfautQTwOPHlQ0B8KAQ&#43;sZZVQOFtfi3EfvbG52vifZb1pWMAWjff/NauicNeNI3p18/JsS6OMOFEE0uSgGfqv2otrnOQSWAqSIUAGSxP1Qr2sMMDGR09/fcI79vhchRz7U22M1eRgw&#43;t5VNO4n1GiH6vXVtn9yZCiF3hVIt33v3sEz0/r/WlZID9dfPNP9olgvglfN5ZzwwhLDIBjK2bYn68N9L22A&#43;3HO7VKIQPsSjs5QgTPrmN81Ew4Wc&#43;VHvX384FcGGd2Vr4TOj9bt078eHQ/RcH8D7L&#43;tIzQL/2pQIu&#43;6H4PCHPDJYn0sWk1iEYQA58fCEHbOoDTp4JhPKdKXxin90nfgX8f&#43;ydPQ7CMAyF7RYQ1ygbU2/DhrgaO5fp3KWHAARSqYyK7cZloxI/If6kblEkkpfEJM4LXiSqPyHCkegxws8cxUunkxidj1NTo&#43;h4JRoBWMpyA2275JdMxWFseNeep4uct5opp7CZNDOjP&#43;flJCSUybTdmVngJucWnXpZyg0zQgwXvvp/dFnWwXxxhao6fLdhJhClACzrYscp2Wq1HnLt0MYP9tMNhFFFobD1sSZbnRYzZrZQ1/tP/dS3EL0AnlkVW9BoP5MbvLpQaBfrujHKKLUHd0OriH&#43;gPP3Sf03ze4Gc40zmRWth599wASSOCyBxXACJ4wJIHBdA4twDAAD//w8Bv3a8xwsaAAAAAElFTkSuQmCC"
     data-sizes="auto"
     width="300" alt="Data Generation Toolkit Logo features Loinheart Astro with bits falling on him above the project label."
     loading="lazy" />


</a>

  
  
</figure></p>
<p>For a little over two years I&rsquo;ve had the privilege of helping lead the <a href="https://github.com/SFDO-Community-Sprints/DataGenerationToolkit">Data Generation Toolkit Project</a> for Salesforce. In general, Open Source projects are not known for their inclusive and supportive communities. I believe it is fair to say our project demonstrates that building a supportive community can yield great results.</p>
<p>Started by a question I wrote on a piece of paper in 2019 and posted to a wall, the project grows more every time I look around. That first meeting inspired the creation of <a href="https://snowfakery.readthedocs.io/en/latest/">Snowfakery</a> and the recent training for Snowfakery has attracted more than 300 registrants. Contributors created <a href="https://sfdo-community-sprints.github.io/DataGenerationToolkit/DataGenGuide.html">documentation</a> and <a href="https://github.com/SFDO-Community-Sprints/DataGenerationToolkit/wiki/Talks---Webinars">presentations</a> to help Salesforce admins learn to seed sandboxes. We are building a <a href="https://github.com/SFDO-Community-Sprints/Snowfakery-Recipe-Templates">recipe library</a> to help people starting out on Snowfakery. We launched <a href="/2021/12/announcing-two-new-snowfakery-faker-providers/">two faker data providers</a>. This year we reviewing and documenting other tools to generate and move Salesforce data.</p>
<p>More importantly we&rsquo;ve built a project that is useful to the community, and supports new contributors to open source projects.</p>
<p>We did not get here accidentally. The project leadership wants to support the community members as much as create new tools. From the beginning we chose to encourage people unfamiliar with open source projects, contributions, and technologies.</p>
<h2 id="leading-an-supportive-open-source-project">Leading an Supportive Open Source Project</h2>
<p>To be a supportive project starts with the leaders.</p>
<p>The project currently has seven identified leaders: <a href="https://trailblazer.me/id/alisa">Alisa Edwards</a>, <a href="https://github.com/allisonletts">Allison Letts</a>, <a href="https://github.com/eehjunggnujhee">Jung Mun</a>, <a href="https://www.linkedin.com/in/paulprescod/">Paul Prescod</a>, <a href="https://thedataarealright.blog/">Samantha Shain</a>, <a href="https://twitter.com/supilowski__c">Cassie Supilowski</a>, and myself. For those who like diversity statistics: that can be seen as 5 women, 2 men, 3 countries of residence, 3 counties of origin (not the same 3), 3 developers, 4 Salesforce admins, and at least 3 racial identities. No sub-group perfectly reflects its community, but that&rsquo;s not a bad start in my opinion.</p>
<p>We have agreed that some of us lead specific sub-projects, while others tend to the health of the community. Everyone has a role.</p>
<p>When we started out with just 4 leaders either Paul (as creator and maintainer of Snowfakery) or I (as the person who started the project) could have dominated the project claiming founder status. Certainly men leading other open source projects have used that status to control the project direction. But <a href="https://developer.salesforce.com/podcast/2022/03/episode-117-open-source-commons-with-cori-o-brien-part-1">Salesforce Open Source Commons</a> projects are designed to discourage that behavior, and Paul and I embraced that design. <a href="https://twitter.com/CoriOPaluck">Cori O&rsquo;Brien</a> created a wonderful space for our project to grow within.</p>
<p>Any open source project should focus on supporting its users. Our goal as a project is to support the Salesforce community – particularly nonprofit and education users. To do that we need the insights that only come from being open to outside ideas.</p>
<p>Our project&rsquo;s leadership also established a pattern of self-review and reflection. Each year we will gather to discuss if the leadership group is the right size, and if anyone needs to step down. That creates a space for us to routinely reflect on what we&rsquo;re doing and if we&rsquo;re doing it well. The invitation to leave frees people from responsibilities they have to the project without frustration or burn out.</p>
<h2 id="recipes-for-open-source-projects">Recipes for Open Source Projects</h2>
<p>Beyond our leadership team structure we also are intentional about how we encourage all contributors.</p>
<p>The project as a whole has space for all kinds of contributions. If someone wants to write documentation we will support them. When someone wants to learn to write recipes, we will work in pairs to get through the first one. People who want to write Python code for our faker providers get the chance to do that too.</p>
<p>We try to be <a href="/2021/09/being-nice-vs-being-kind/">kind</a> to all contributors. We thank <em>everyone</em> when they open pull-requests or issues. Even <em>I</em> get thanked when I open a PR. Even if someone needs to make significant changes to a contribution before we commit it, we make sure to praise their effort.</p>
<p>These aren&rsquo;t <a href="https://knowyourteam.com/blog/2017/12/01/dont-want-to-be-a-bad-manager-stop-feeding-shit-sandwiches/">shit sandwiches</a>. We genuinely appreciate all contributions and divorce that from any corrections that we request. As a long time open source contributor I am surprised at how much I appreciate the messages.</p>
<h3 id="maintaining-supportive-discussion-channels">Maintaining Supportive Discussion Channels</h3>
<p>Most open source maintainers know we have to maintain good ways for contributors to ask questions. Over time projects have used a wide variety of tools: IRC, News Groups, Email, issue trackers, and now Slack. In most projects those are the spaces that tend to become ugly. You see RTFM-style answers, personal criticisms, identity-based attacks, and other ugliness. The Slack channels for our project are a key piece of how we engage with each other, and support new contributors. We work hard to make sure people get timely answers, clear directions, and steady encouragement.</p>
<p>Our work falls within a community where our reputations matter and so there is a level of decorum absent in some open source projects. But no group is perfect and we will address issues when they arise. The open source common&rsquo;s DEI Framework project will hopefully help us continue to deepen our understanding of the community. The kinds of attacks open source community spaces frequently allow in the name of good code, are simply unacceptable.</p>
<h2 id="come-join-us">Come Join Us</h2>
<p>The <a href="https://trailhead.salesforce.com/en/trailblazer-community/feed/0D54S00000G2iaPSAR">next community sprint</a> <a href="https://trailhead.salesforce.com/trailblazer-community/feed/0D54S00000G4ef2SAB">for</a> <a href="https://trailhead.salesforce.com/en/trailblazer-community/feed/0D54S00000G2iaPSAR">our project is coming up May 4th &amp; 5th.</a></p>
]]></content:encoded> </item> <item>
      <title>Salesforce Electron Starter</title>
      <link>https://spinningcode.org/2020/12/salesforce-electron-starter/</link>
      <pubDate>
        Sat, 26 Dec 2020 21:48:20 +0000
      </pubDate> <guid
        isPermaLink="false">https://spinningcode.org/?p=1520</guid>  <description>I would like to introduce Electron Salesforce Base, a secure base-line for Electron projects with Salesforce.</description> <content:encoded><![CDATA[<p>Back in August I created an <a href="/2020/08/simple-electron-starter/">Electron project starter</a> that provides a template to use for electron projects with the goal of outlining how to follow the current best practices for writing secure electron apps. I had extracted that from a couple of personal projects I work on from time to time, one of those projects is <a href="https://github.com/acrosman/electronForce">ElectronForce</a> – a tool to explore Salesforce orgs.  Because I get ideas of things I want to try out from time to time as Salesforce APIs applications I have now created a derivative project that is setup to create apps that leverage <a href="https://jsforce.github.io/">JSForce</a> to interact with Salesforce orgs.</p>
<p>Thus I would like to introduce <a href="https://github.com/acrosman/electron-sf-base">Electron Salesforce Base</a>.</p>
<p>Like my generic project starter it is intended to be a jumping off point that handles some of the basic elements of a project.  It’s a bit more opinionated because comes with a little more plumbing in place – there is a simple interface and it’ll actually log into orgs (assuming you have your <a href="https://help.salesforce.com/articleView?id=user_security_token.htm&amp;type=5">security token</a>).</p>
<p>The interface is built using a Bootstrap dark theme from <a href="https://bootswatch.com/">Bootswatch</a>, and is set up to follow the Airbnb ESLint standards (with a couple small tweaks). The interface generates two windows, one that is meant to be the main interface and includes the controls to log into your org, and a second that is meant to keep a running log of events.</p>
<p>The main thread is fully isolated from the render threads, with all requests and data being passed back and forth using the current inter process communication methodology from Electron leveraging the <a href="https://www.electronjs.org/docs/api/ipc-main">IPCmain</a> object in the main thread and the <a href="https://www.electronjs.org/docs/api/context-bridge">contextBridge</a> in the render thread – there is no access to <a href="https://medium.com/@nornagon/electrons-remote-module-considered-harmful-70d69500f31">remote</a> in the render thread (actually remote is fully disabled as it should be), and the <code>preload.js</code> file largely serves to filter IPC requests to maintain thread isolation. Currently the main thread of this project isn’t what I would call graceful, and I’m actually working on a refactor for ElectronForce to improve the IPC listener definitions (readers with examples of good design patterns are more than welcome to offer suggestions, whatever I settle on will likely get folded back into this project eventually).</p>
<p>To help understand the general pattern that&rsquo;s emerging as people get better at sandboxing in Electron (and Electron gets better at demanded it) I find it helpful to think of Electron apps in a client-server model with two largely separate applications and a well defined API for communication between them. You’ll see that reflected here, and in my other recent Electron apps. You can implement whatever you’d like in each layer and just pass messages back and forth. This also means you can totally refactor one part of your application without worrying about the other. So if you hate my proto-interface dump it and build something better.</p>
<p>If you look at the code for this base project, then look at ElectronForce, you will see the render thread provides all the details of the interface – including use of render-friendly libraries like jQuery and a collection of helper functions to make life a little easier – makes an API call (with a filter list provided in <code>preload.js</code>), and then handles responses from the main thread. In <code>main.js</code> you see all the IPC listeners defined, which then pass the needed data to JSForce to make the API calls, before handing back structured data for the interface to render.</p>
<p>As you dig through the code you may notice various <code>@TODO</code> statements that are notes to myself about places with obvious room for improvement. I’m always happy to get suggestions, as comments here, issues there, or as pull requests to help resolve those notes with better solutions.</p>
]]></content:encoded> </item> <item>
      <title>Salesforce Queries and Proxies in Drupal 8</title>
      <link>https://spinningcode.org/2019/10/salesforce-queries-and-proxies-in-drupal-8/</link>
      <pubDate>
        Mon, 28 Oct 2019 14:04:57 +0000
      </pubDate> <guid
        isPermaLink="false">https://spinningcode.org/?p=1263</guid>  <description>How to query Salesforce from Drupal and proxy the results as JSON responses.</description> <content:encoded><![CDATA[<p>The Drupal 8 version of the Salesforce Suite provides a powerful combination of features that are ready to use and mechanisms for adding custom add-ons you may need.  What it does not yet have is lots of good public documentation to explain all those features.</p>
<p>A recent support issue in the <a href="https://www.drupal.org/project/salesforce/issues/3088430">Salesforce issue queue asked for example code</a> for writing queries. While I’ll address some of that here, there is ongoing work to <a href="https://www.drupal.org/project/salesforce/issues/2879844">replace the query interface</a> to be more like Drupal core’s.  Hopefully once that’s complete I’ll get a chance to revise this article, but be warned some of those details may be a little out of date depending on when you read this post.</p>
<p>To run a simple query for all closed Opportunities related to an Account that closed after a specific date you can do something like the following:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>      $query <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">SelectQuery</span>(<span style="color:#e6db74">&#39;Opportunity&#39;</span>);
</span></span><span style="display:flex;"><span>      $query<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">fields</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;Id&#39;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;Name&#39;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;Description&#39;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;CloseDate&#39;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;Amount&#39;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;StageName&#39;</span>,
</span></span><span style="display:flex;"><span>      ];
</span></span><span style="display:flex;"><span>      $query<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">addCondition</span>(<span style="color:#e6db74">&#39;AccountId&#39;</span>, $desiredAccountId, <span style="color:#e6db74">&#39;=&#39;</span>);
</span></span><span style="display:flex;"><span>      $query<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">conditions</span>[] <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;(StageName&#34;</span>, <span style="color:#e6db74">&#39;=&#39;</span>, <span style="color:#e6db74">&#34;&#39;Closed Won&#39;&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;OR&#39;</span>, <span style="color:#e6db74">&#39;StageName&#39;</span>, <span style="color:#e6db74">&#39;=&#39;</span>, <span style="color:#e6db74">&#34;&#39;Closed Lost&#39;)&#34;</span>,
</span></span><span style="display:flex;"><span>      ];
</span></span><span style="display:flex;"><span>      $query<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">conditions</span>[] <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#39;CloseDate&#39;</span>, <span style="color:#e6db74">&#39;&gt;=&#39;</span>, $someSelectedDate];
</span></span><span style="display:flex;"><span>      $sfResponse <span style="color:#f92672">=</span> <span style="color:#a6e22e">\Drupal</span><span style="color:#f92672">::</span><span style="color:#a6e22e">service</span>(<span style="color:#e6db74">&#39;salesforce.client&#39;</span>)<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">query</span>($query);
</span></span></code></pre></div><p>The class would need to include a use statement for to get <code>Drupal\salesforce\SelectQuery;</code> And ideally you would embed this in a service that would allow you to inject the Salesforce Client service more correctly, but hopefully you get the idea.</p>
<p>The main oddity in the code above is the handling of query conditions (which is part of what lead to the forthcoming interface changes). You can use the <code>addCondition()</code> method and provide a field name, value, and comparison as lie 10 does. Or you can add an array of terms directly to the conditions array that will be imploded together. Each element of the conditions array will be ANDed together, so OR conditions need to be inserted the way lines 11-14 handle it.</p>
<p>Running a query in the abstract is pretty straight forward, the main question really is what are you going to do with the data that comes from the query. The suite’s main mapping features provide most of what you need for just pulling down data to store in entities, and you should use the entity mapping features until you have a really good reason not to, so the need for direct querying is somewhat limited.</p>
<p>But there are use cases that make sense to run queries directly. Largely these are around pulling down data that needs to be updated in near-real time (so perhaps that list of opportunities would be ones related to my user that were closed in the last week instead of some random account).</p>
<p>I’ve written about using <a href="/2017/05/cached-json-responses-in-drupal-8/">Drupal 8 to proxy remote APIs</a> before. If you look at the sample code you’ll see the comment that says: <code>// Do some useful stuff to build an array of data</code>.  Now is your chance to do something useful:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#f92672">&lt;?</span><span style="color:#a6e22e">php</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">namespace</span> <span style="color:#a6e22e">Drupal\example\Controller</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Symfony\Component\HttpFoundation\Request</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Drupal\Core\Controller\ControllerBase</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Drupal\Core\Cache\CacheableJsonResponse</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Drupal\Core\Cache\CacheableMetadata</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ExampleController</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">ControllerBase</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getJson</span>(<span style="color:#a6e22e">Request</span> $request) {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Securely load the AccountId you want, and set date range.
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        $data <span style="color:#f92672">=</span> [];
</span></span><span style="display:flex;"><span>        $query <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">SelectQuery</span>(<span style="color:#e6db74">&#39;Opportunity&#39;</span>);
</span></span><span style="display:flex;"><span>        $query<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">fields</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;Id&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;Name&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;Description&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;CloseDate&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;Amount&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;StageName&#39;</span>,
</span></span><span style="display:flex;"><span>        ];
</span></span><span style="display:flex;"><span>        $query<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">addCondition</span>(<span style="color:#e6db74">&#39;AccountId&#39;</span>, $desiredAccountId, <span style="color:#e6db74">&#39;=&#39;</span>);
</span></span><span style="display:flex;"><span>        $query<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">conditions</span>[] <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;(StageName&#34;</span>, <span style="color:#e6db74">&#39;=&#39;</span>, <span style="color:#e6db74">&#34;&#39;Closed Won&#39;&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;OR&#39;</span>, <span style="color:#e6db74">&#39;StageName&#39;</span>, <span style="color:#e6db74">&#39;=&#39;</span>, <span style="color:#e6db74">&#34;&#39;Closed Lost&#39;)&#34;</span>,
</span></span><span style="display:flex;"><span>        ];
</span></span><span style="display:flex;"><span>        $query<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">conditions</span>[] <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#39;CloseDate&#39;</span>, <span style="color:#e6db74">&#39;&gt;=&#39;</span>, $someSelectedDate];
</span></span><span style="display:flex;"><span>        $sfResponse <span style="color:#f92672">=</span> <span style="color:#a6e22e">\Drupal</span><span style="color:#f92672">::</span><span style="color:#a6e22e">service</span>(<span style="color:#e6db74">&#39;salesforce.client&#39;</span>)<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">query</span>($query);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#66d9ef">empty</span>($sfResponse)) {
</span></span><span style="display:flex;"><span>        $data[<span style="color:#e6db74">&#39;opp_count&#39;</span>] <span style="color:#f92672">=</span> $sfResponse<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">size</span>();
</span></span><span style="display:flex;"><span>        $data[<span style="color:#e6db74">&#39;opps&#39;</span>] <span style="color:#f92672">=</span> [];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> ($data[<span style="color:#e6db74">&#39;opp_count&#39;</span>]) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">foreach</span> ($sfResponse<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">records</span>() <span style="color:#66d9ef">as</span> $opp) {
</span></span><span style="display:flex;"><span>                $data[<span style="color:#e6db74">&#39;opps&#39;</span>][] <span style="color:#f92672">=</span> $opp<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">fields</span>();
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>      $data[<span style="color:#e6db74">&#39;opp_count&#39;</span>] <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Add Cache settings for Max-age and URL context.
</span></span></span><span style="display:flex;"><span>    <span style="color:#75715e">// You can use any of Drupal&#39;s contexts, tags, and time.
</span></span></span><span style="display:flex;"><span>    $data[<span style="color:#e6db74">&#39;#cache&#39;</span>] <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;max-age&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">600</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;contexts&#39;</span> <span style="color:#f92672">=&gt;</span> [
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;url&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;user&#39;</span>,
</span></span><span style="display:flex;"><span>        ],
</span></span><span style="display:flex;"><span>    ];
</span></span><span style="display:flex;"><span>    $response <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">CacheableJsonResponse</span>($data);
</span></span><span style="display:flex;"><span>    $response<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">addCacheableDependency</span>(<span style="color:#a6e22e">CacheableMetadata</span><span style="color:#f92672">::</span><span style="color:#a6e22e">createFromRenderArray</span>($data));
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> $response;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="cautions-and-considerations">Cautions and Considerations</h2>
<p>I left out a couple details above on purpose. Most notable I am not showing ways to get the needed SFID for filtering because you need to apply a little security checking on your route/controller/service. And those checks are probably specific to your project. If you are not careful you could let anonymous users just explore your whole database. It is an easy mistake to make if you do something like use a Salesforce ID as a URL parameter of some kind. You will want to make sure you know who is running queries and that they are allowed to see the data you are about to present. <em>This is on you as the developer, not on Drupal or Salesforce, and I&rsquo;m not risking giving you a bad example to follow.</em></p>
<p>Another detail to note is that I used the cache response for a reason.  Without caching every request would go through to Salesforce. This is both slower than getting cached results (their REST API is not super fast and you are proxying through Drupal along the way), and leaves you open to a simple DOS where someone makes a bunch of calls and sucks up all your API requests for the day. Think carefully before limiting or removing those cache options (and make sure your cache actually works in production).  Setting a context of both URL and User can help ensure the right people see the right data at the right time.</p>
]]></content:encoded> </item> <item>
      <title>Drupal Salesforce Suite Custom Field Mapping Types</title>
      <link>https://spinningcode.org/2019/10/drupal-salesforce-suite-custom-field-mapping-types/</link>
      <pubDate>
        Thu, 17 Oct 2019 11:00:59 +0000
      </pubDate> <guid
        isPermaLink="false">https://spinningcode.org/?p=1254</guid>  <description>Creating your own custom field mapping between Drupal and Salesforce using the Drupal Salesforce Suite.</description> <content:encoded><![CDATA[<p>The <a href="https://www.drupal.org/project/salesforce">Drupal 8 Salesforce Suite</a> allows you to map Drupal entities to Salesforce objects using a 1-to-1 mapping. To do this it provides a series of field mapping types that allow you to select how you want to relate the data between the two systems. Each field type provides handling to help ensure the data is handled correctly on each side of the system.</p>
<p>As of this writing the suite provides six usable field mapping types:</p>
<ul>
<li>Properties — The most common type to handle mapping data fields.</li>
<li>Record Type — A special handler to support Salesforce record type settings when needed.</li>
<li>Related IDs — Handles translating SFIDs to Drupal Entity IDs when two objects are related in both systems.</li>
<li>Related Properties — For handling properties across a relationship (when possible).</li>
<li>Constant — A constant value on the Drupal side that can be pushed to Salesforce.</li>
<li>Token — A value set via Drupal Token.</li>
</ul>
<p>There is a seventh called Broken to handle mappings that have changed and need a fallback until its fixed. The <code>salesforce_examples</code> module also includes a <a href="https://git.drupalcode.org/project/salesforce/blob/8.x-4.x/modules/salesforce_example/src/Plugin/SalesforceMappingField/Hardcoded.php">very simple example called Hardcoded</a> the shows how to create a mapping with a fixed value (similar to, but less powerful than, Constant field).</p>
<p>These six handle the vast majority of use cases but not all.  Fortunately the suite was designed using <a href="/2017/01/drupal-8-plugins-are-addictive/">Drupal 8 annotated plugins</a> , so you can add your own as needed. There is an example in the suite’s example module, and you can review the code of the ones that are included, but I think some people would find an overview helpful.</p>
<p>As an example I’m using the plugin I created to add support for <a href="https://www.drupal.org/project/salesforce/issues/3047748">related entities to the webform submodule of the suite</a> (I’m referencing the patch in <a href="https://www.drupal.org/files/issues/2019-07-23/salesforce-webform_entity_references-3047748-10.patch">#10</a> cause that’s current as of this writing, but you should actually use whatever version is most recent or been accepted).</p>
<p>Like all good annotated plugins to tell Drupal about it all we have to do is create the file in the right place. In this case that is: <code>[my_module_root]/src/Plugins/SalesforceMappingField/[ClassName]</code> or more specifically: <code>salesforce_webform/src/Plugin/SalesforceMappingField/WebformEntityElements.php</code></p>
<p>At the top of the file we need to define the namespace, add some use statements.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#f92672">&lt;?</span><span style="color:#a6e22e">php</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">namespace</span> <span style="color:#a6e22e">Drupal\salesforce_webform\Plugin\SalesforceMappingField</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Drupal\Core\Entity\EntityInterface</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Drupal\Core\Form\FormStateInterface</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Drupal\salesforce_mapping\Entity\SalesforceMappingInterface</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Drupal\salesforce_mapping\SalesforceMappingFieldPluginBase</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Drupal\salesforce_mapping\MappingConstants</span>;
</span></span></code></pre></div><p>Next we need to provide the required annotation for the plugin manager to use. In this case it just provides the plugin’s ID, which needs to be unique across all plugins of this type, and a translated label.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#e6db74">/**
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> * Adapter for Webform elements.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> *
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> * @Plugin(
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> *   id = &#34;WebformEntityElements&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> *   label = @Translation(&#34;Webform entity elements&#34;)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> * )
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> */</span>
</span></span></code></pre></div><p>Now we define the class itself which must extend <code>SalesforceMappingFieldPluginBase</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">WebformEntityElements</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">SalesforceMappingFieldPluginBase</span> {
</span></span></code></pre></div><p>With those things in place we can start the real work.  The mapping field plugins are made up of a few parts:</p>
<ul>
<li>The configuration form elements which display on the mapping settings edit form.</li>
<li>A value function to provide the actual outbound value from the field.</li>
<li>Nice details to limit when the mapping should be used, and support dependency management.</li>
</ul>
<p>The <code>buildConfigurationForm</code> function returns an array of form elements. The base class provides some basic pieces of that array that you should plan to use and modify. So first we call the function on that parent class, and then make our changes:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span> <span style="color:#e6db74">/**
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   * {@inheritdoc}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   */</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">buildConfigurationForm</span>(<span style="color:#66d9ef">array</span> $form, <span style="color:#a6e22e">FormStateInterface</span> $form_state) {
</span></span><span style="display:flex;"><span>    $pluginForm <span style="color:#f92672">=</span> <span style="color:#66d9ef">parent</span><span style="color:#f92672">::</span><span style="color:#a6e22e">buildConfigurationForm</span>($form, $form_state);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    $options <span style="color:#f92672">=</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getConfigurationOptions</span>($form[<span style="color:#e6db74">&#39;#entity&#39;</span>]);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">empty</span>($options)) {
</span></span><span style="display:flex;"><span>      $pluginForm[<span style="color:#e6db74">&#39;drupal_field_value&#39;</span>] <span style="color:#f92672">+=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;#markup&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">t</span>(<span style="color:#e6db74">&#39;No available webform entity reference elements.&#39;</span>),
</span></span><span style="display:flex;"><span>      ];
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>      $pluginForm[<span style="color:#e6db74">&#39;drupal_field_value&#39;</span>] <span style="color:#f92672">+=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;#type&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;select&#39;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;#options&#39;</span> <span style="color:#f92672">=&gt;</span> $options,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;#empty_option&#39;</span> <span style="color:#f92672">=&gt;</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">t</span>(<span style="color:#e6db74">&#39;- Select -&#39;</span>),
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;#default_value&#39;</span> <span style="color:#f92672">=&gt;</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">config</span>(<span style="color:#e6db74">&#39;drupal_field_value&#39;</span>),
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;#description&#39;</span> <span style="color:#f92672">=&gt;</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">t</span>(<span style="color:#e6db74">&#39;Select a webform entity reference element.&#39;</span>),
</span></span><span style="display:flex;"><span>      ];
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Just allowed to push.
</span></span></span><span style="display:flex;"><span>    $pluginForm[<span style="color:#e6db74">&#39;direction&#39;</span>][<span style="color:#e6db74">&#39;#options&#39;</span>] <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">MappingConstants</span><span style="color:#f92672">::</span><span style="color:#a6e22e">SALESFORCE_MAPPING_DIRECTION_DRUPAL_SF</span> <span style="color:#f92672">=&gt;</span> $pluginForm[<span style="color:#e6db74">&#39;direction&#39;</span>][<span style="color:#e6db74">&#39;#options&#39;</span>][<span style="color:#a6e22e">MappingConstants</span><span style="color:#f92672">::</span><span style="color:#a6e22e">SALESFORCE_MAPPING_DIRECTION_DRUPAL_SF</span>],
</span></span><span style="display:flex;"><span>    ];
</span></span><span style="display:flex;"><span>    $pluginForm[<span style="color:#e6db74">&#39;direction&#39;</span>][<span style="color:#e6db74">&#39;#default_value&#39;</span>] <span style="color:#f92672">=</span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">MappingConstants</span><span style="color:#f92672">::</span><span style="color:#a6e22e">SALESFORCE_MAPPING_DIRECTION_DRUPAL_SF</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> $pluginForm;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  }
</span></span></code></pre></div><p>In this case we are using a helper function to get us a list of entity reference fields on this plugin (details are in the patch and unimportant to this discussion). We then make those fields the list of Drupal fields for the settings form. The array we got from the parent class already provides a list of Salesforce fields in <code>$pluginForm[‘salesforce_field’]</code> so we don’t have to worry about that part.  Since the salesforce_webform module is push-only on its mappings, this plugin was designed to be push only as well, and so limits to direction options to be push only. The default set of options is:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#e6db74">&#39;#options&#39;</span> <span style="color:#f92672">=&gt;</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">MappingConstants</span><span style="color:#f92672">::</span><span style="color:#a6e22e">SALESFORCE_MAPPING_DIRECTION_DRUPAL_SF</span> <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">t</span>(<span style="color:#e6db74">&#39;Drupal to SF&#39;</span>),
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">MappingConstants</span><span style="color:#f92672">::</span><span style="color:#a6e22e">SALESFORCE_MAPPING_DIRECTION_SF_DRUPAL</span> <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">t</span>(<span style="color:#e6db74">&#39;SF to Drupal&#39;</span>),
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">MappingConstants</span><span style="color:#f92672">::</span><span style="color:#a6e22e">SALESFORCE_MAPPING_DIRECTION_SYNC</span> <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">t</span>(<span style="color:#e6db74">&#39;Sync&#39;</span>),
</span></span><span style="display:flex;"><span> ],
</span></span></code></pre></div><p>And you can limit those anyway that makes sense for your plugin.</p>
<p>With the form array completed, we now move on to the value function. This is generally the most interesting part of the plugin since it does the work of actually setting the value returned by the mapping.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>  <span style="color:#e6db74">/**
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   * {@inheritdoc}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   */</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">value</span>(<span style="color:#a6e22e">EntityInterface</span> $entity, <span style="color:#a6e22e">SalesforceMappingInterface</span> $mapping) {
</span></span><span style="display:flex;"><span>    $element_parts <span style="color:#f92672">=</span> <span style="color:#a6e22e">explode</span>(<span style="color:#e6db74">&#39;__&#39;</span>, $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">config</span>(<span style="color:#e6db74">&#39;drupal_field_value&#39;</span>));
</span></span><span style="display:flex;"><span>    $main_element_name <span style="color:#f92672">=</span> <span style="color:#a6e22e">reset</span>($element_parts);
</span></span><span style="display:flex;"><span>    $webform <span style="color:#f92672">=</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">entityTypeManager</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getStorage</span>(<span style="color:#e6db74">&#39;webform&#39;</span>)<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">load</span>($mapping<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#39;drupal_bundle&#39;</span>));
</span></span><span style="display:flex;"><span>    $webform_element <span style="color:#f92672">=</span> $webform<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getElement</span>($main_element_name);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span>$webform_element) {
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">// This reference field does not exist.
</span></span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      $value <span style="color:#f92672">=</span> $entity<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getElementData</span>($main_element_name);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      $referenced_mappings <span style="color:#f92672">=</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">mappedObjectStorage</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">loadByDrupal</span>($webform_element[<span style="color:#e6db74">&#39;#target_type&#39;</span>], $value);
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#66d9ef">empty</span>($referenced_mappings)) {
</span></span><span style="display:flex;"><span>        $mapping <span style="color:#f92672">=</span> <span style="color:#a6e22e">reset</span>($referenced_mappings);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> $mapping<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">sfid</span>();
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">\Exception</span> $e) {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">NULL</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span></code></pre></div><p>In this case we are finding the entity referred to in the webform submission, loading any mapping objects that may exist for that entity, and returning the Salesforce ID of the mapped object if it exists.  Yours will likely need to do something very different.</p>
<p>There are actually two related functions defined by the plugin interface, defined in the base class, and available for override as needed for setting pull and push values independently:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>  <span style="color:#e6db74">/**
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   * An extension of ::value, ::pushValue does some basic type-checking and
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   * validation against Salesforce field types to protect against basic data
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   * errors.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   *
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   * @param \Drupal\Core\Entity\EntityInterface $entity
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   * @param \Drupal\salesforce_mapping\Entity\SalesforceMappingInterface $mapping
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   *
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   * @return mixed
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   */</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">pushValue</span>(<span style="color:#a6e22e">EntityInterface</span> $entity, <span style="color:#a6e22e">SalesforceMappingInterface</span> $mapping);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">/**
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   * An extension of ::value, ::pullValue does some basic type-checking and
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   * validation against Drupal field types to protect against basic data
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   * errors.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   *
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   * @param \Drupal\salesforce\SObject $sf_object
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   * @param \Drupal\Core\Entity\EntityInterface $entity
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   * @param \Drupal\salesforce_mapping\Entity\SalesforceMappingInterface $mapping
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   *
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   * @return mixed
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   */</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">pullValue</span>(<span style="color:#a6e22e">SObject</span> $sf_object, <span style="color:#a6e22e">EntityInterface</span> $entity, <span style="color:#a6e22e">SalesforceMappingInterface</span> $mapping);
</span></span></code></pre></div><p>But be careful overriding them directly. The base class provides some useful handling of various data types that need massaging between Drupal and Salesforce, you may lose that if you aren’t careful. I encourage you to look at the details of both <a href="https://git.drupalcode.org/project/salesforce/blob/8.x-4.x/modules/salesforce_mapping/src/SalesforceMappingFieldPluginBase.php#L172">pushValue</a> and <a href="https://git.drupalcode.org/project/salesforce/blob/8.x-4.x/modules/salesforce_mapping/src/SalesforceMappingFieldPluginBase.php#L251">pullValue</a> before working on those.</p>
<p>Okay, with the configuration and values handled, we just need to deal with programmatically telling Drupal when it can pull and push these fields. Most of the time you don’t need to do this, but you can simplify some of the processing by overriding pull() and push() to make sure the have the right response hard coded instead of derived from other sources. In this case pulling the field would be bad, so we block that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>  <span style="color:#e6db74">/**
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   * {@inheritdoc}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   */</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">pull</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">FALSE</span>;
</span></span><span style="display:flex;"><span>  }
</span></span></code></pre></div><p>Also, we only want this mapping to appear as an option if the site has the webform module enabled. Without it there is no point in offering it at all. The plugin interface provides a function called <code>isAllowed()</code> for this purpose:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>  <span style="color:#e6db74">/**
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   * {@inheritdoc}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   */</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">isAllowed</span>(<span style="color:#a6e22e">SalesforceMappingInterface</span> $mapping) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">\Drupal</span><span style="color:#f92672">::</span><span style="color:#a6e22e">service</span>(<span style="color:#e6db74">&#39;module_handler&#39;</span>)<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">moduleExists</span>(<span style="color:#e6db74">&#39;webform&#39;</span>);
</span></span><span style="display:flex;"><span>  }
</span></span></code></pre></div><p>You can also use that function to limit a field even more tightly based on the mapping itself.</p>
<p>To further ensure the configuration of this mapping entity defines its dependencies correctly we can define additional dependencies in <code>getDependencies()</code>. Again here we are tied to the Webform module and we should enforce that during and config exports:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>  <span style="color:#e6db74">/**
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   * {@inheritdoc}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">   */</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getDependencies</span>(<span style="color:#a6e22e">SalesforceMappingInterface</span> $mapping) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> [<span style="color:#e6db74">&#39;module&#39;</span> <span style="color:#f92672">=&gt;</span> [<span style="color:#e6db74">&#39;webform&#39;</span>]];
</span></span><span style="display:flex;"><span>  }
</span></span></code></pre></div><p>And that is about it.  Once the class exists and is properly setup, all you need to do is rebuild the caches and you should see your new mapping field as an option on your Salesforce mapping objects (at least when isAllowed() is returning true).</p>
]]></content:encoded> </item> <item>
      <title>FormAssembly Dynamic Parameter Signing</title>
      <link>https://spinningcode.org/2019/04/formassembly-dynamic-parameter-signing/</link>
      <pubDate>
        Sun, 28 Apr 2019 16:12:29 +0000
      </pubDate> <guid
        isPermaLink="false">https://spinningcode.org/?p=1051</guid>  <description>PHP Function to support FormAssembly dynamic parameter signing.</description> <content:encoded><![CDATA[<p>For a project I’ve been working on recently we had need to create a module that provides secure redirects from a Drupal site to <a href="https://www.formassembly.com/">FormAssembly</a>. Overall the module does a number of things, but handling dynamic parameter signing was the thing that took the most time.</p>
<p>FormAssembly provides a variety of great features for creating flexible forms that integrate with Salesforce. One of the more popular features is its ability to pull data from Salesforce to prefill fields on a form. But the downside is that it is easy to create forms that leak information from Salesforce into those forms, and create privacy risks.</p>
<p>To address this, FormAssembly allows 3rd party tools to securely sign URLs that contain parameters (often Salesforce IDs) that could be used to extract information through an iteration attack and other basic approaches. This <a href="https://help.formassembly.com/help/508885-prefill-lookups-using-secure-parameters">secure signing process</a> can be done statically but for most interesting projects you want to sign the URLs dynamically. The dynamic signing process allows you alter the parameters on the fly and set an expiration date to limit the value of a stolen link. Our project required this approach.</p>
<p>But the dynamic signing process has a couple sharp corners. First, it’s rarely done outside of Salesforce so there aren’t a lot of code samples around, and none that I could find in PHP.  Second, FormAssembly is very open and honest about the fact that they do not provide support on this feature. So I had to create my own process from the documentation they provide.  The docs are good, but very Salesforce centric, with all code samples in APEX.</p>
<p>The process involves preparing the data for signature, generating a HMAC-SHA256 with a form specific pre-shared key (in binary mode), converted to a string using base64, and finally URL encode the result.</p>
<p>Their convention for preparing the data is straightforward. You format all parameters as just their key and value strung together: <code>key1Value1key2Value2</code></p>
<p>The interesting part is the actual HMAC-SHA256, which needs to be generated in binary mode, something that is often the default mode but not in PHP (in fact most PHP devs I’ve talked don’t realize the last parameter to <a href="https://www.php.net/manual/en/function.hash-hmac.php">hash_hmac()</a> is useful, if you are doing this in another language check out <a href="https://www.jokecamp.com/blog/examples-of-creating-base64-hashes-using-hmac-sha256-in-different-languages/">this collection of examples</a>).</p>
<p>From there you encode the output in base-64 (which results in a 44 character hash), and URL encode the hash to make sure it’s URL safe, and you’ll end up a few characters longer.</p>
<p>Finally you add you hash to the query string, and you’re ready to go.</p>
<p>To help anyone else who needs to do this, I generalized this part of the solution and I created and tossed it into Gist.</p>
<script
  src="https://gist.github.com/acrosman/ff85c0a180caa0bc0dc2d83bbd9e7a80.js"></script>
]]></content:encoded> </item> </channel>
</rss>
