[{"content":"\rThe Configuration Challenge\rCreating, updating and translating picklist values in Salesforce can be a time-consuming exercise. In this article, I will:\nReview how this is done manually via the Salesforce Setup UI. Briefly examine the metadata XML file that can be extracted and deployed via the Salesforce Command Line Interface (CLI). Demonstrate how an Excel-based data dictionary can be used as input for the SfMetadataToolkit to generate deployment-ready metadata. Via The Salesforce Setup UI\rThis requires multiple steps in the Setup UI. A typical sequence to add and translate values looks like this:\nNavigate to the Global Value Set / Picklist Value Set: Click New to add new picklist values. This is the only step in the sequence where a bulk-operation is possible by copying and pasting multiple values in one go. If you want the Master Label to differ from the API value, then go through all values one by one and click Edit to update Label and/or API Name. If you want to reorder the values to a specific sequence, click Reorder to move individual values up and down, one click at a time. If you then want to translate the labels, you have to go to a different setup screen: Navigate to the Translation Workbench: Select the first language. Navigate to Setup Component \u0026lsquo;Global Value Set\u0026rsquo;. Select the value set and wait to see the current translations. Click through all translations to add/update. Click Save. If you need this for another language, repeat the Translation Workbench process. Obviously, this is not an efficient approach for more than some one-off changes.\nAn example for a picklist value set as shown in the Salesforce Setup UI\rAn example for a picklist edit operation in the Salesforce Setup UI\rAn example for a picklist translation in the translation workbench\rVia the Salesforce Command Line Interface (CLI)\rWith the Salesforce Command Line Interface (CLI, also known as SFDX), you get direct access to the raw XML version of the metadata, in this case a Picklist Value Set, in your local file system:\nRetrieve the files via sf project retrieve command(s), e.g. as given below. Edit the XML file(s) as needed. (Re-)Deploy with the corresponding counterpart sf project deploy start ... 1 2 3 sf project retrieve start -m GlobalValueSet:MyPicklistValueSet sf project retrieve start -m GlobalValueSetTranslation:MyPicklistValueSet-en sf project retrieve start -m GlobalValueSetTranslation:MyPicklistValueSet-de Example output for the preceding retrieve command\r1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;GlobalValueSet xmlns=\u0026#34;http://soap.sforce.com/2006/04/metadata\u0026#34;\u0026gt; \u0026lt;customValue\u0026gt; \u0026lt;fullName\u0026gt;A\u0026lt;/fullName\u0026gt; \u0026lt;default\u0026gt;false\u0026lt;/default\u0026gt; \u0026lt;label\u0026gt;A - Master Label\u0026lt;/label\u0026gt; \u0026lt;/customValue\u0026gt; \u0026lt;customValue\u0026gt; \u0026lt;fullName\u0026gt;B\u0026lt;/fullName\u0026gt; \u0026lt;default\u0026gt;false\u0026lt;/default\u0026gt; \u0026lt;label\u0026gt;B - Master Label\u0026lt;/label\u0026gt; \u0026lt;/customValue\u0026gt; \u0026lt;customValue\u0026gt; \u0026lt;fullName\u0026gt;C\u0026lt;/fullName\u0026gt; \u0026lt;default\u0026gt;false\u0026lt;/default\u0026gt; \u0026lt;label\u0026gt;C - Master Label\u0026lt;/label\u0026gt; \u0026lt;/customValue\u0026gt; \u0026lt;masterLabel\u0026gt;My Picklist Value Set\u0026lt;/masterLabel\u0026gt; \u0026lt;sorted\u0026gt;false\u0026lt;/sorted\u0026gt; \u0026lt;/GlobalValueSet\u0026gt; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;GlobalValueSetTranslation xmlns=\u0026#34;http://soap.sforce.com/2006/04/metadata\u0026#34;\u0026gt; \u0026lt;valueTranslation\u0026gt; \u0026lt;masterLabel\u0026gt;A - Master Label\u0026lt;/masterLabel\u0026gt; \u0026lt;translation\u0026gt;A - German Translation\u0026lt;/translation\u0026gt; \u0026lt;/valueTranslation\u0026gt; \u0026lt;valueTranslation\u0026gt; \u0026lt;masterLabel\u0026gt;B - Master Label\u0026lt;/masterLabel\u0026gt; \u0026lt;translation\u0026gt;B - German Translation\u0026lt;/translation\u0026gt; \u0026lt;/valueTranslation\u0026gt; \u0026lt;valueTranslation\u0026gt; \u0026lt;masterLabel\u0026gt;C - Master Label\u0026lt;/masterLabel\u0026gt; \u0026lt;translation\u0026gt;C - German Translation\u0026lt;/translation\u0026gt; \u0026lt;/valueTranslation\u0026gt; \u0026lt;/GlobalValueSetTranslation\u0026gt; Obviously, this isn\u0026rsquo;t an efficient approach either because you now have to deal with XML syntax and its associated pitfalls.\nIntroducing SfMetadataToolkit\rThis approach at a glance:\nFill your picklist values, labels and translations into an Excel (or a corresponding .csv) template. Run the toolkit\u0026rsquo;s importer and builder commands to create the corresponding metadata XML files Optionally, run the diff tool of your choice (e.g. git diff) to see changes. Use Salesforce CLI to deploy. The full benefit is realized if you use the spreadsheet as a Data Dictionary holding the source of truth: The Data Dictionary is the documentation that is always up to date because the metadata is automatically derived from it.\nBuild Metadata From The Excel-based Template\rFill in the worksheet _GlobalValueSets to declare the Global Picklist Value Sets to be created:\nWorksheet ‘_GlobalValueSets’ of the Excel template\rFill in the worksheet _PicklistValues and for each Picklist Value Set, define the order, values, labels and translations as needed:\nWorksheet ‘_PicklistValues’ of the Excel template\rRun the following toolkit commands:\n1 2 3 $Metadata = Import-SfMetadataModel -Path .\\EXAMPLE-DataDictionary.xlsx Build-SfGlobalValueSetMetadata $Metadata Build-SfGlobalValueSetTranslationMetadata $Metadata -LanguageCodes (\u0026#39;de\u0026#39;, \u0026#39;en\u0026#39;) Build Metadata From The CSV Template\rThe toolkit also accepts CSV files as long as those are structured the same way. So, if you prefer sources other than Excel, here is the way to go.\nPrepare the file _GlobalValueSets.csv:\nExample file _GlobalValueSets.csv\rPrepare the file _PicklistValues.csv:\nExample file _PicklistValues.csv\rPut all relevant CSV files together into one directory and run the following toolkit commands:\n1 2 3 $Metadata = Import-SfMetadataModel -Path .\\csv-example\\ Build-SfGlobalValueSetMetadata $Metadata Build-SfGlobalValueSetTranslationMetadata $Metadata -LanguageCodes (\u0026#39;de\u0026#39;, \u0026#39;en\u0026#39;) Examine and Diff the Result Files as Needed\rThe resulting files in the force-app folder look like this:\nResulting force-app folder as shown in Visual Studio Code\rDeploy\u0026hellip;:\n1 sf project deploy start -m GlobalValueSet GlobalValueSetTranslation \u0026hellip;and enjoy:\nThe deployed picklist value sets as per Salesforce Setup UI\rA single example deployed picklist value set as per Salesforce Setup UI\rDeployed picklist label translations as per Translation Workbench in Salesforce Setup UI\rHow It Works\rThe SfMetadataToolkit operates on top of the Salesforce project structure managed by Salesforce CLI. It does not communicate directly with your Salesforce org nor does it issue any sf cli commands on its own. Instead, it only reads from and writes to the *.*-meta.xml files in the force-app folder. So it can be combined with any DevOps/deployment workflow, IDE and diff tool (git or non-git-based) of your choice.\n📝\rNote\rIf you don\u0026rsquo;t yet have a full developer setup: The minimum requirement is the CLI itself. You don\u0026rsquo;t have to set up VS Code (Notepad++ with XML plugin will do), you don\u0026rsquo;t need git (any file diff tool of your choice will do, e.g. Notepad++) and you don\u0026rsquo;t need to configure a Dev Hub.\nYou can use it for any one-off mass-creation of picklist values. The full strength of this approach, however, becomes apparent when you let the data dictionary be the source of truth for your implementation. With the metadata created from the documentation, the documentation and the implementation will always be in sync.\nWith certain limitations, it will also support a round-trip approach where the data dictionary Excel can be reverse-engineered from the org\u0026rsquo;s metadata.\nHow To Get It\rThe module is based on PowerShell 7.x, which is open source and available on Windows, macOS, and Linux:\nInstall PowerShell on Windows, Linux, and macOS PowerShell on GitHub What is PowerShell? The SfMetadataToolkit is published on Codeberg and on the PowerShell Gallery:\nSfMetadataToolkit To install: Install-Module -Name SfMetadataToolkit Codeberg Repo PowerShell directly supports importing from and exporting to CSV files. To work with .xlsx files, the ImportExcel module needs to be installed from the PowerShell Gallery:\nImportExcel To install: Install-Module -Name ImportExcel GitHub Repo What\u0026rsquo;s Next\rThis Data-Dictionary-driven approach has been battle-tested in various projects over the past years. The PowerShell code is currently being refactored and cleaned from some technical debt in order to be published as open source gradually. So stay tuned to see more functionality to come.\n","date":"2026-07-17T00:00:00Z","permalink":"/sf-metadata-toolkit-bulk-manage-global-value-sets/","title":"SfMetadataToolkit: Bulk manage Picklist Values, Labels and Translations in Salesforce with Excel"},{"content":"Multi-Select Picklists can be tricky to handle when it comes to validations. In this article I will show how a Flow with the \u0026ldquo;Custom Error Message\u0026rdquo; element can be used for a validation logic that can be way more complex than with the \u0026ldquo;classic\u0026rdquo; validation rules.\nThe example requirement is to create a validation message upon record save for a multi-select picklist field \u0026ldquo;Product Interest\u0026rdquo; as follows:\nWe want to allow users to choose additional values, e.g. add \u0026ldquo;Dairy Products — Butter\u0026rdquo; to the \u0026ldquo;chosen\u0026rdquo; list. But we want to prevent users from un-selecting values, e.g. remove \u0026ldquo;Fruits — Apples\u0026rdquo; from the \u0026ldquo;chosen\u0026rdquo; list. Multi-select picklist field \u0026ldquo;Product Interest\u0026rdquo;.\nThe Data Model\rThe example will be shown with a multi-select picklist custom field on the Lead object:\n1 2 3 4 API Name: ProductInterest__c Label: Product Interest Data Type: Picklist (Multi-Select) Global Value Set: Product Groups The global value set \u0026ldquo;Product Groups\u0026rdquo;:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Values API Name ------------------------- --------------- Dairy Products - Butter Dairy.03 Dairy Products - Cheese Dairy.04 Dairy Products - Milk Dairy.01 Dairy Products - Yogurt Dairy.02 Fruits - Apples Fruits.01 Fruits - Bananas Fruits.02 Fruits - Oranges Fruits.04 Fruits - Strawberries Fruits.03 Sweets - Chocolate bars Sweets.01 Sweets - Cookies Sweets.02 Vegetables - Broccoli Vegetables.02 Vegetables - Carrots Vegetables.01 Vegetables - Spinach Vegetables.03 The Flow\rSee below the preview of the flow we are about to build:\nThe Validation Flow\nStart Element\rCreate a \u0026ldquo;Record-Triggered Flow\u0026rdquo; that runs before the record is saved. (Will also work with an after-save flow if this better fits your use case.)\nObject: Lead (or any other standard or custom object) Trigger the Flow When: \u0026ldquo;A record is updated\u0026rdquo; Condition Requirements: Field \u0026ldquo;Product Interest\u0026rdquo; Is Changed = True Start Element configuration\nHelper Constant\rDefine a Constant as follows for clearer reference inside the Flow:\nSection \u0026ldquo;Constants\u0026rdquo;\nDefine the semicolon \u0026ldquo;;\u0026rdquo; as a constant PicklistValueDelimiter\nHelper Variables\rWe will need two helper text variables:\nSection \u0026ldquo;Variables\u0026rdquo;\ndefined as follows:\nText Variable: CurrentPicklistValues\nText Variable: PriorPicklistValues\nGet the Object Definition\rA Get Records element to get the object \u0026ldquo;Entity Definition\u0026rdquo; with the definition of the object we are working on, here: Lead.\ngetObjectDefinition\nIf you want to do this with a custom object, don\u0026rsquo;t forget to add \u0026ldquo;__c\u0026rdquo; to the API Name, e.g. \u0026ldquo;MyCustomObject__c\u0026rdquo;.\nGet the Field Definition\rA Get Records element to query the object \u0026ldquo;Field Definition\u0026rdquo; for our multi-select picklist field \u0026ldquo;ProductInterest__c\u0026rdquo; using the \u0026ldquo;Durable Id\u0026rdquo; we\u0026rsquo;ve got in the previous step:\ngetFieldDefinition\nGet the Active Picklist Values\rA Get Records element to query the object \u0026ldquo;Picklist Value Info\u0026rdquo; for all currently active picklist values for this field:\ngetActivePicklistValues\nNotes:\nMake sure the setting under \u0026ldquo;How Many Records to Store\u0026rdquo; reads \u0026ldquo;All records\u0026rdquo; (unlike the other Get Records elements before). This will only return active picklist values. There is no way to query for inactive values with this method. In case you are using Record Types, also be aware: This will only return picklist values for the current record type of the current record. Convert the Picklist Field Values to Delimiter-separated Text Variables\rAn Assignment Element as follows:\nconvertPicklistToText\nEn detail, this reads:\n1 2 3 4 {!CurrentPicklistValues} Equals {!$Record.ProductInterest__c} {!PriorPicklistValues} Equals {!$Record__Prior.ProductInterest__c} {!CurrentPicklistValues} Add {!PicklistValueDelimiter} {!PriorPicklistValues} Add {!PicklistValueDelimiter} Loop Through the Active Picklist Values\rNow we loop through all the values as per record collection returned by getActivePicklistValues:\nloopPicklistValues\nFor each known picklist value, we check whether it was present in the PRIOR value but is missing in the CURRENT value. If such case occurs, we know that we need to raise the validation error message towards the user:\nCheck_Has_it_been_removed\nThe condition logic reads \u0026ldquo;1 AND NOT 2\u0026rdquo; with the following conditions:\n1 2 {!PriorPicklistValues} Contains {!LoopPicklistValueWithTrailingDelimiter} {!CurrentPicklistValues} Contains {!LoopPicklistValueWithTrailingDelimiter} where \u0026ldquo;LoopPicklistValueWithTrailingDelimiter\u0026rdquo; is a helper text formula defined as follows:\n1 {!loopPicklistValues.Value} + {!PicklistValueDelimiter} Validation Error Message\rUpon the first occurrence of such case, we use the \u0026ldquo;Custom Error Message\u0026rdquo; element to raise the validation error to the user and stop the flow:\nValidationMessage\nShow Case\rNow let\u0026rsquo;s run a test: Go to the example Lead, click \u0026ldquo;Edit\u0026rdquo; and play around with the multi-select picklist fields. Once you try to save the record after un-choosing a picklist value, the validation error message will be shown:\nTest Run\nConclusion and Considerations\rThe \u0026ldquo;Custom Error Message\u0026rdquo; flow element has the same effect as a traditional validation rule: Once fired, the save transaction will fail and the user (in case of a UI transaction) or the caller of the interface (in case of an API/Dataloader transaction) will get the custom error message. Record-triggered flows are way more flexible for implementing complex validation logic compared to the formula field-style \u0026ldquo;classic\u0026rdquo; validation rules. Always be aware of the \u0026ldquo;order of execution\u0026rdquo;: In our example, we\u0026rsquo;ve used a before-save flow as this gives best performance in such scenario and we know that, by our design, no other logic will touch the picklist values on our record. But you need to assess whether other parts of other flows might update the picklist fields and you need to put the validation elsewhere in the order of execution, e.g. by setting \u0026ldquo;Trigger Order\u0026rdquo; in the flow properties and/or switching to an after-save flow. The \u0026ldquo;Get Records\u0026rdquo; on \u0026ldquo;Picklist Value Info\u0026rdquo; only ever will see and return active picklist values for the given record type. Hence, we cannot identify the (very special) case where the user decides to un-choose a value that is a now-inactive picklist value: We don\u0026rsquo;t know about it, hence we won\u0026rsquo;t check for it in our loop. This article has been written as per Summer \u0026lsquo;25 release.\nFor more information about handling picklist values in Flow also see the article \u0026ldquo;How to get Picklist Labels in Salesforce Flow\u0026rdquo;.\n","date":"2025-07-14T00:00:00Z","permalink":"/how-to-build-a-complex-validation-rule-for-multi-select-picklist-in-salesforce-flow/","title":"How to Build a Complex Validation Rule for Multi-Select Picklist in Salesforce Flow"},{"content":"Salesforce Flow has come a long way and there have been vast improvements over the years and still there are interesting challenges when it comes to working with collections. But sometimes Flow is way more powerful than expected if you are willing to dig out a not-so-obvious solution.\nThe challenge here is: How to get from a list/collection of strings/records to a single comma-separated (.csv) formatted string?\nThe .csv Challenge\nIn this post I will present various options and discuss the pros and cons:\nThe Basic Approach: a very simple one with just one flow formula needed to convert a text collection into a comma-separated string. The Basic Approach with custom delimiters: an extended version of the first approach that is a bit more flexible, but still fits into a single flow formula. The Full Approach — with a text collection as the source: a fully featured approach that covers all special cases w.r.t. escape sequences and delimiter configuration, but requires more steps than just a single flow formula. The Full Approach — with a record collection as the source: same approach, slightly different steps to use the Transform element instead of a Loop. All options shown are contained in a sample demo Screen Flow that can be found here: CSV_Concatenation_Screen_Flow.flow-meta.xml\nNow let\u0026rsquo;s see, what we can get out of example data as shown below:\n1 2 3 4 5 Payload #1 Payload #2 Payload #3 with \u0026#34;1\u0026#34;,\u0026#34;2\u0026#34;,\u0026#34;3\u0026#34; commas Payload #4 with \u0026#34;string\u0026#34; delims Payload #5 with \u0026#34;;\u0026#34; semicolon delim The Basic Approach\rWe take a flow variable of type string collection as starting point and populate it with some test values.\nMyTextCollection flow variable.\nPopulate text collection with the demo values.\nTo convert this string collection to a comma-separated string, we just have to use it in a flow formula and do some simple post-processing to remove the [ at the beginning and the ] at the end that the formula puts there by default. See flow text formula toCsvBasicApproach:\n1 2 /* Cut off leading/trailing [ and ] */ MID({!MyTextCollection}, 2, LEN({!MyTextCollection}) - 2) What we get is:\n1 Payload #1,Payload #2,Payload #3 with \u0026#34;1\u0026#34;,\u0026#34;2\u0026#34;,\u0026#34;3\u0026#34; commas,Payload #4 with \u0026#34;string\u0026#34; delims,Payload #5 with \u0026#34;;\u0026#34; semicolon delim Considerations\rThis is quick and simple and works very well, if, and only if, the following constraints are ok for your use case:\nPayload SHOULD NOT contain \u0026quot; because this might or might not confuse the parser of the consumer, depending on its exact implementation. However, we should generally consider this as .csv non-best practice. Hence, do not use if the payload may contain \u0026quot; characters. Payload MUST NOT contain any comma , because with this approach the formulas cannot distinguish payload comma from field delimiting comma, so the whole result string will be broken once any payload string contains even a single comma. Field delimiter is fixed to comma , Generally no string delimiter. Or, to put it simple, this will work great for converting e.g.\n1 2 3 Text 1 Text 2 Text 3 into Text 1,Text 2,Text 3\nThe Basic Approach with Custom Delimiters\rFor this improved approach, we want to have control over field delimiter and string delimiter. So we define two text variables, FieldDelimiter and StringDelimiter and initialize accordingly:\nAssign desired field delimiter and string delimiters.\nAnd we now use an enhanced formula toCsvBasicWithDelimitersApproach to convert our text collection:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 {!StringDelimiter} /* leading string delimiter */ + SUBSTITUTE( /* outer replace: comma field delimiter to target field delimiter plus string delimiter */ SUBSTITUTE( /* inner replace: pre-processing to escape string delimiters by duplicating it */ {!ToCsvBasicApproach}, {!StringDelimiter}, {!StringDelimiter} + {!StringDelimiter} ), \u0026#34;,\u0026#34;, {!StringDelimiter} + {!FieldDelimiter} + {!StringDelimiter} ) + {!StringDelimiter} /* trailing string delimiter */ What we get:\n1 \u0026#34;Payload #1\u0026#34;;\u0026#34;Payload #2\u0026#34;;\u0026#34;Payload #3 with \u0026#34;\u0026#34;1\u0026#34;\u0026#34;,\u0026#34;\u0026#34;2\u0026#34;\u0026#34;,\u0026#34;\u0026#34;3\u0026#34;\u0026#34; commas\u0026#34;;\u0026#34;Payload #4 with \u0026#34;\u0026#34;string\u0026#34;\u0026#34; delims\u0026#34;;\u0026#34;Payload #5 with \u0026#34;\u0026#34;;\u0026#34;\u0026#34; semicolon delim\u0026#34; Considerations\rThis delivers some minor improvements:\nWe can configure field delimiter and string delimiter. We get the string delimiter properly escaped if contained in payload. However, we still cannot deal with , comma as part of the payload: The replace operation still cannot distinguish between the comma created by concatenation and the comma that comes as part of the payload.\nAgain, to put it simple, works well for\n1 2 3 Text 1 Text 2 Text \u0026#34;Special Case\u0026#34; 3 to be converted into \u0026quot;Text 1\u0026quot;;\u0026quot;Text 2\u0026quot;;\u0026quot;Text \u0026quot;\u0026quot;Special Case\u0026quot;\u0026quot; 3\u0026quot;\nThe Full Approach\rTo further improve, we need to pre-process the payload strings first before doing the concatenation. The pre-processing allows to handle literally all combinations of string delimiter and field delimiter also being part of the payload.\nThere are two options, depending on where the string collection originates from:\nCase A: To pre-process a text collection, we put a Loop around our text collection with a single assignment element inside the loop. The actual work is done within a formula field again. Case B: If the payload data happens to originate from the text field of a record collection, it is easier to combine two Transform elements to do the trick without the need for a loop. (N.B.: This will use an enhancement of the Transform element available in Winter \u0026lsquo;25.) Case A: Text Collection as the Source\rIn total, the steps in the flow are:\nPut together the text collection as in the previous examples. Loop through the text collection with one assignment element inside the loop for a formula to do the pre-processing. Access the resulting text collection via Formula that does the post-processing into a single .csv formatted string. The Loop will take the initial text collection…\nLoop through text collection\n… and access each source text item via the formula toCsvPreprocessedLoopItem that does the pre-processing…\n1 2 3 4 5 6 7 8 9 10 11 12 13 {!StringDelimiter} /* each payload to start with string delimiter... */ + SUBSTITUTE(SUBSTITUTE( SUBSTITUTE({!toCsvPreprocessorLoop}, {!StringDelimiter}, {!StringDelimiter} + {!StringDelimiter} /* escape StringDelimiter inside payload by duplicating it everywhere */ ), \u0026#34;%\u0026#34;, URLENCODE(\u0026#34;%\u0026#34;)), /* first urlencode % sign inside payload */ \u0026#34;,\u0026#34;, URLENCODE(\u0026#34;,\u0026#34;)) /* then urlencode standard comma field delimiter inside payload */ + {!StringDelimiter} /* ...and also end with string delimiter */ …and collects it into a new text collection named PreprocessedTextCollection:\nAssignment element inside the loop\nAll items in the text collection now are pre-processed as follows:\nAll string delimiters are escaped by duplicating them, e.g. \u0026quot; to \u0026quot;\u0026quot; All % characters inside the payload are URL-encoded to %25 All comma , characters are URL-encoded to %2C Whole payload enclosed in string delimiters, e.g. \u0026quot;payload\u0026quot; Now we can access this pre-processed text collection via the formula toCsvPreprocessorApproach…\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 SUBSTITUTE(SUBSTITUTE( SUBSTITUTE( MID({!PreprocessedTextCollection}, 2, LEN({!PreprocessedTextCollection}) - 2) /* strip off starting/leading [/] */ , {!StringDelimiter} + \u0026#34;,\u0026#34; + {!StringDelimiter}, /* replace default field delimiter... */ {!StringDelimiter} + {!FieldDelimiter} + {!StringDelimiter} /* ...by our own one. Note: field delimiter inside payload is URLencoded */ ), URLENCODE(\u0026#34;,\u0026#34;), \u0026#34;,\u0026#34;), /* revert urlencoding of comma inside payload */ URLENCODE(\u0026#34;%\u0026#34;), \u0026#34;%\u0026#34;) /* revert urlencoding of % character */ …to get this built-to-order and properly escaped .csv string:\n1 \u0026#34;Payload #1\u0026#34;;\u0026#34;Payload #2\u0026#34;;\u0026#34;Payload #3 with \u0026#34;\u0026#34;1\u0026#34;\u0026#34;,\u0026#34;\u0026#34;2\u0026#34;\u0026#34;,\u0026#34;\u0026#34;3\u0026#34;\u0026#34; commas\u0026#34;;\u0026#34;Payload #4 with \u0026#34;\u0026#34;string\u0026#34;\u0026#34; delims\u0026#34;;\u0026#34;Payload #5 with \u0026#34;\u0026#34;;\u0026#34;\u0026#34; semicolon delim\u0026#34; Case B: Record Collection as the Source\rIf the source data comes from record collection anyway, we can avoid the loop by having the Transform element do the tricks.\nIn total, the steps in the flow are:\nGet Records element to retrieve the Source Data, here: Accounts Transform element #1 to pre-process the desired field, here: Account.Name Transform element #2 to put all Account.Name fields into a simple text collection. Access the resulting text collection via Formula that does the post-processing into a single .csv formatted string. We start with getting the Account records in question via a Get Records element. For this example we assume, that it returns 4 Accounts with Account names as given in our example data above.\nGet Records Element\nThen we use a Transform element that will take the Accounts collection from the previous step and will return another Accounts collection for temporary use. This Target Data collection will have the pre-processed version of Account name.\nTransform Element #1\nIt will map Account.Name from Source Data to Account.Name in Target Data with the following formula for the pre-processing:\n1 2 3 4 5 6 7 8 9 10 11 12 13 {!StringDelimiter} /* each payload to start with string delimiter... */ + SUBSTITUTE(SUBSTITUTE( SUBSTITUTE({!getAccounts[$EachItem].Name}, {!StringDelimiter}, {!StringDelimiter} + {!StringDelimiter} /* escape StringDelimiter inside payload by duplicating it everywhere */ ), \u0026#34;%\u0026#34;, URLENCODE(\u0026#34;%\u0026#34;)), /* first urlencode % sign inside payload */ \u0026#34;,\u0026#34;, URLENCODE(\u0026#34;,\u0026#34;)) /* then urlencode standard comma field delimiter inside payload */ + {!StringDelimiter} /* ...and also end with string delimiter */ We get our Account names pre-processed as follows:\nAll string delimiters are escaped by duplicating them, e.g. \u0026quot; to \u0026quot;\u0026quot; All % characters inside the payload are URL-encoded to %25 All comma , characters are URL-encoded to %2C Whole payload enclosed in string delimiters, e.g. \u0026quot;payload\u0026quot; With the next Transform element, we will take this temporary Accounts collection as Source Data and transform all Account.Name to a simple text collection:\nTransform Element #2\nSimilar to the Basic Approach at the very beginning, we take this text collection as input for a formula field that does all the post-processing to deliver the final .csv formatted string:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 SUBSTITUTE(SUBSTITUTE( SUBSTITUTE( MID({!PreprocessedTextCollection}, 2, LEN({!PreprocessedTextCollection}) - 2) /* strip off starting/leading [/] */ , {!StringDelimiter} + \u0026#34;,\u0026#34; + {!StringDelimiter}, /* replace default field delimiter... */ {!StringDelimiter} + {!FieldDelimiter} + {!StringDelimiter} /* ...by our own one. Note: field delimiter inside payload is URLencoded */ ), URLENCODE(\u0026#34;,\u0026#34;), \u0026#34;,\u0026#34;), /* revert urlencoding of comma inside payload */ URLENCODE(\u0026#34;%\u0026#34;), \u0026#34;%\u0026#34;) /* revert urlencoding of % character */ Here we go — from this selection of Account names…\nAccounts Collection\n…we get this built-to-order and properly escaped .csv string:\n1 \u0026#34;Payload #1\u0026#34;;\u0026#34;Payload #2\u0026#34;;\u0026#34;Payload #3 with \u0026#34;\u0026#34;1\u0026#34;\u0026#34;,\u0026#34;\u0026#34;2\u0026#34;\u0026#34;,\u0026#34;\u0026#34;3\u0026#34;\u0026#34; commas\u0026#34;;\u0026#34;Payload #4 with \u0026#34;\u0026#34;string\u0026#34;\u0026#34; delims\u0026#34;;\u0026#34;Payload #5 with \u0026#34;\u0026#34;;\u0026#34;\u0026#34; semicolon delim\u0026#34; Considerations\rThe \u0026ldquo;full\u0026rdquo; approach properly handles all potential field delimiters and string delimiters that might occur inside the payload. So it is supposed to be suitable irrespective of actual content of the payload.\nString delimiter and field delimiter are configurable. New Transform element as of Winter \u0026lsquo;25 simplifies pre-processing without the need for a Loop element in certain cases. In cases where we still rely on a Loop, we benefit from the fact, that the former limit \u0026ldquo;execution of maximum number of elements per flow interview = 2000\u0026rdquo; has been removed in Spring \u0026lsquo;23. ","date":"2024-09-23T00:00:00Z","permalink":"/how-to-concatenate-a-collection-of-strings-to-a-comma-delimited-string-in-salesforce-flow/","title":"How to Concatenate a Collection of Strings to a Comma-Delimited String in Salesforce Flow"},{"content":" When working with picklist fields in Salesforce Flows, e.g. in a formula or a text template, the flow can only \u0026ldquo;see\u0026rdquo; the picklist API value and not the picklist label as shown in the GUI. This might not be an issue in your org if API value and label are configured to be always same and translation workbench is not in use. In all other cases, this can create usability constraints, e.g. when it comes to showing picklist values in Screen Flows or sending e-mails from within the Flow.\nI will discuss the following solutions:\nUse the Picklist screen component to extract the label. Use a set of Get Record elements to query the PicklistValueInfo standard object in order to find the label for a given picklist value. Use an invocable action in Flow to have an APEX class return the information from metadata. All options in this article focus on handling single records and getting labels to display information on the screen or use text templates, for example to put together e-mails to be sent by the flow.\nThere are additional challenges when it comes to working with record collections, for example, to be used in the Data Table screen component. There might be a future article about this.\nThe Basic Scenario\rA Salesforce org with the translation workbench enabled. Active languages are English, German and Spanish. Translations are in place for English and German only. A custom object MyCustomObject__c and its corresponding custom tab. A custom picklist field MyPicklist__c using the global value set MyPicklistValueSet. This global value set and its translations are shown in the table below. MyPicklistValueSet\nOur example record looks like this:\nExample Record\nWe do a simple flow that gets the example record by its record ID and puts its fields on a screen component:\nBasic Scenario Flow\nThe screen component will show:\nThe Field component for the custom field MyPicklist__c of our example record. A Display Text component that outputs the content of\na) the picklist field directly,\nb) a text formula field referencing the picklist field,\nc) a text template referencing the picklist field. Field Component\nDisplay Text Component\nText Formula\nText Template\nAnd here comes what is wrong with it when we run this flow: Although the field component properly renders the picklist field with its (translated) label as we would expect it, all further references to the picklist only show the API value.\nFlow Output\nSolution #1: Picklist Choice Set\rThere is a difference between putting the Field Component of the picklist custom field on a screen and putting a Picklist Choice Set on a screen. As opposed to the basic scenario, here we use the Picklist Choice Set:\nAdd a new ressource of type Picklist Choice Set. Set Object to the custom object where our picklist field is located. Set the picklist field. MyPicklistChoiceSet\nNow the first component in our screen will be a picklist component that will refer to above Picklist Choice Set:\nMultiple Options = No Component Type = Picklist Choice = link to Picklist Choice Set as created above Default Value = The picklist field value from our example record. Plus: Add the Picklist Component in the Display Text section.\nPicklist Component\nOur debug run will now look like this:\nFlow Output\nThe text output of the Picklist Component will also work in formulas and text templates.\nConsiderations for this approach:\nObviously it will only work in screen flows, i.e. not suitable for record-triggered flows nor scheduled flows. The Picklist Component must be rendered and visited during the screen flow, i.e. it MUST be shown to the user. It could be set to read-only (Disabled = {!$GlobalConstant.True}), but not to hidden. Otherwise it won\u0026rsquo;t be initialized with its default value and will be empty on accessing it. This might or might not be compatible with your specific use case. Not suitable for record collections, especially for showing picklist labels in the Data Table component. Per-Transaction Flow Limits: Does not count towards \u0026ldquo;Total number of SOQL queries issued\u0026rdquo;. It will always deliver the label for the language of the running user, i.e. either the Master Label or the label as per translation workbench. Solution #2: „Get Record\u0026quot; to Query PicklistValueInfo\rIn this approach, we will query the standard objects EntityDefinition, FieldDefinition and PicklistValueInfo with the \u0026ldquo;Get Record\u0026rdquo; element in order to get the picklist label:\nQuery for the object definition of our object MyCustomObject__c Query for the field definition of our custom field MyPicklist__c Query for the picklist value info (including the label) for the value in our example record. Flow with PicklistValueInfo\nHere the details for getObjectDefinition:\ngetObjectDefinition\nWith the DurableId from there, we can query the field definition:\ngetFieldDefinition\nAnd with the DurableId of the field definition we can finally query for the Picklist Value Info:\ngetPicklistValueInfo\nNow we are able to reference the picklist label from the result of getPicklistValueInfo and use it in the screen display text or formula fields or text templates:\nDisplay Text Screen Component\nOur debug run will now look like this:\nFlow Output\nConsiderations for this approach:\nIndependent of any screen components, i.e. suitable for screen flows, record-triggered flows and scheduled flows. Per-Transaction Flow Limits: Each Get Record element counts as a SOQL query towards the \u0026ldquo;Total number of SOQL queries issued\u0026rdquo; limit. So be aware when it comes to getting labels for multiple picklist fields and/or with looping, for example, when working for record collections and the Data Table screen component. It will always deliver the label for the language of the running user, i.e. either the Master Label or the label as per translation workbench. Solution #3: Invocable Apex Methods\rFor this approach, we need APEX code that can access the Schema objects and return labels for API values. Such APEX class can provide its methods as an \u0026ldquo;Invocable Method\u0026rdquo; which in turn can be accessed in a flow action.\nA good summary explanation for invocable actions can be found under https://www.salesforceben.com/salesforce-invocable-apex-methods-a-complete-breakdown/ and an example implementation for such approach can be found under https://unofficialsf.com/flow-action-get-picklist-values/.\nConsiderations:\nAll pros and cons of APEX coding, e.g. the need for developer capability and capacity and having proper test classes and code coverage in place. Per-Transaction Flow Limits: Does not consume SOQL queries within the \u0026ldquo;Total number of SOQL queries issued\u0026rdquo; limit. Suitable for screen flows, scheduled flows and record-triggered after-save flows. Not suitable for record-triggered before-save flows as those do not allow sub flows and actions. Conclusion\rThese approaches are basically just workarounds for a, from my view, major lack of feature in Salesforce flow. It definitely should be a standard feature to be able to chose whether we want to see the API value or the label in the flow. I suggest to vote the following ideas on the IdeaExchange:\nhttps://ideas.salesforce.com/s/idea/a0B8W00000PJ4sTUAT/show-picklist-label-instead-of-api-value-in-flow-datatable https://ideas.salesforce.com/s/idea/a0B8W00000QN6h3UAD/show-value-or-trans-of-picklist-field-picked-for-data-table-column-screen-flow ","date":"2024-08-16T00:00:00Z","permalink":"/how-to-get-picklist-labels-in-salesforce-flow/","title":"How to get Picklist Labels in Salesforce Flow"}]