The call was logged. The nurture tag silently never happened.

A subflow tags a builder for nurture when the call outcome is Existing Installer. The first formula would not deploy because it touched a multi-select picklist. The second deployed, ran without a fault, updated every other field in the same assignment, and wrote null. How it was found, why getVariableValue could not see it, and the nested IF that fixed it.

The builder said “we already have an installer, call me after the new year”. The rep logged the call. The follow-up moved two months out, the call showed up in the activity history, and the prospect was supposed to land on the list of builders worth nurturing.

The tag that puts him there was never written. No error, no fault email, no red banner. Every other field in the same step updated exactly as designed. One field got null, and because it was already empty, null looked like nothing at all.

This is how that was found, and why it took four attempts to get a one-line idea right.

What the board is

At Doorvana, new-construction sales starts with cold outreach to home builders. The prospecting board is built on Lead: builder prospects sit on the same Service_Lead record type as everyone else and are split out by Customer_Type__c = Builder. A Log Outreach quick action launches a screen flow; the screen flow hands its inputs to an autolaunched subflow, Log_Outreach_Apply, which does all the work. The split is deliberate — the rules can be exercised headlessly from Apex, and a future telephony or import path can call the same subflow.

Log_Outreach_Apply creates a completed Task for the history and stamps the current state onto the Lead, because list views cannot filter on Task fields. Every value is pre-decided by a formula resource, and one Assignment element writes them all.

One rule in it: when the outcome is Existing Installer, add Existing Vendor to the multi-select picklist Nurturing_Reason__c, without dropping any reason already there. The Prospects – Nurture list view filters on that field with includes.

Attempt one: the deploy refused it

The natural formula concatenates the field directly. The deploy said no:

The formula expression is invalid: Field Get_Lead is a multi-select picklist field.
Multi-select picklist fields are only supported in certain functions.

That one is fair and well documented. In a flow formula a multi-select picklist field is only usable inside a handful of functions — INCLUDES, ISBLANK, ISNULL and a few others. No &, no CONTAINS. The standard workaround is to copy the field into a Text variable first and build the formula on the variable:

force-app/main/default/flows/Log_Outreach_Apply.flow-meta.xml

<assignments>
    <name>Copy_Nurturing_Reason</name>
    <label>Copy Nurturing Reason</label>
    <assignmentItems>
        <assignToReference>v_NurturingReason</assignToReference>
        <operator>Assign</operator>
        <value>
            <elementReference>Get_Lead.Nurturing_Reason__c</elementReference>
        </value>
    </assignmentItems>
    <connector>
        <targetReference>Stamp_Lead</targetReference>
    </connector>
</assignments>

With the formula reading v_NurturingReason instead of the field, it deployed cleanly.

Attempt two: it deployed, it ran, it wrote null

The acceptance script drives the subflow from anonymous Apex with Flow.Interview.createInterview('Log_Outreach_Apply', inputs).start(), then reloads the Lead and checks each field. Five fabricated builder cases, 35 checks. One failed:

FAIL C2 tagged Nurturing_Reason__c Existing Vendor => null

Case 2 is the existing-installer builder. In the same run, on the same record, through the same Assignment element, Last_Outreach_Outcome__c was Existing Installer, the follow-up date was two months out, the status had moved to Working - Engaged. Only the nurture tag was missing.

The first thing to rule out was the field itself. A two-line anonymous Apex probe inside a savepoint set Nurturing_Reason__c = 'Existing Vendor' directly, read it back, and rolled back. The value stuck. So the field, the value set and the permissions were fine; the formula was handing the assignment nothing.

Attempt three: you cannot read what is not an output

The obvious next step is to ask the interview what the formula evaluated to. Flow.Interview has a method for exactly that:

Flow.Interview fi = Flow.Interview.createInterview('Log_Outreach_Apply', inputs);
fi.start();
System.debug(fi.getVariableValue('v_NurturingReason'));

It returned null — which was useless, because getVariableValue only returns variables marked Available for output. A private variable or a formula resource comes back null whether it holds a value or not. The method cannot distinguish “empty” from “you are not allowed to see this”, and it does not say which.

A debug copy of the subflow did exactly that: the formula’s result assigned to a new output variable, nothing else changed. That settled it. On the path where the Lead’s existing Nurturing_Reason__c was empty, the formula itself evaluated to null.

Attempt four: BLANKVALUE did not help, nesting did

The first reasonable fix is to make sure the variable is never null going in — wrap the variable in BLANKVALUE(). That deployed and changed nothing. The result was still null.

What worked was restructuring the formula so ISBLANK is tested first, in its own IF, and CONTAINS is only reached when the value is already known to be non-blank:

force-app/main/default/flows/Log_Outreach_Apply.flow-meta.xml

<formulas>
    <name>f_NurturingReason</name>
    <dataType>String</dataType>
    <expression>IF({!outcome} &lt;&gt; &quot;Existing Installer&quot;, {!v_NurturingReason},
  IF(ISBLANK({!v_NurturingReason}), &quot;Existing Vendor&quot;,
    IF(CONTAINS({!v_NurturingReason}, &quot;Existing Vendor&quot;), {!v_NurturingReason}, {!v_NurturingReason} &amp; &quot;;Existing Vendor&quot;)
  )
)</expression>
</formulas>

Read top to bottom it is three questions, each only asked if the previous one let it through:

  1. Is this an existing-installer outcome? If not, keep whatever is there.
  2. Is the current value blank? Then the answer is just Existing Vendor.
  3. Only now — with a value known to exist — does it already contain the tag? Keep it, or append ;Existing Vendor.

The semicolon matters: a multi-select picklist is stored as semicolon-separated values, so assigning Timing;Existing Vendor back to the field is how you add a value without dropping one.

The rerun was 35 of 35, including the check that a second Existing Installer log does not duplicate the tag.

Why this one could have shipped

Everything a normal deploy checks passed. The metadata was valid, the flow activated, the interview ran without a fault, and every other field in the step updated correctly. A person clicking through the quick action once would have seen the Task land, the follow-up move, the status change — and would not have thought to open the multi-select and check a field that started out empty.

The list view hides part of the damage, which is worse rather than better. The Prospects – Nurture view has an OR that also matches Last_Outreach_Outcome__c = Existing Installer, so right after the call the builder does appear. But Last_Outreach_Outcome__c is current state: the next logged call, a text or a voicemail in two months, overwrites it. From that point the tag is the only thing keeping him on the list, and the tag was never written. The failure would have surfaced months later as a builder who quietly fell off the nurture list, which is about the least traceable shape a bug can take.

What the acceptance script pins down

The script is anonymous Apex, re-runnable, and builds its own fabricated records — invented builders such as Brazos Bend Builders — deleting the previous run’s first. The cases that bear on this post:

scripts/apex/prospecting-board-acceptance.apex

logOutreach(c2.Id, today, 'Call', 'Existing Installer',
    'Happy with current installer. Open to a call after the new year.',
    null, 'Check in on installer relationship', today.addMonths(2));
Lead l2 = reload(c2.Id);
check('C2 tagged Nurturing_Reason__c Existing Vendor',
      l2.Nurturing_Reason__c != null && l2.Nurturing_Reason__c.contains('Existing Vendor'),
      l2.Nurturing_Reason__c);

// A second Existing Installer log must not duplicate the nurture reason.
logOutreach(c2.Id, today, 'Text', 'Existing Installer', null, null, null, null);
l2 = reload(c2.Id);
check('C2 nurture reason not duplicated',
      l2.Nurturing_Reason__c == 'Existing Vendor', l2.Nurturing_Reason__c);

The second log also passes blank inputs on purpose. A blank input on Log Outreach means “keep what the Lead already has” — finishing a call must never silently clear the next follow-up — and the same case asserts the two-month follow-up survived it. Same family of bug, opposite direction: a formula that turns nothing entered into something erased.

Practical summary

  • In a flow formula, a multi-select picklist field is only allowed inside a few functions. Copy it to a Text variable with an Assignment element before concatenating or using CONTAINS.
  • A formula that evaluates to null does not fault the interview. The assignment writes null and the flow reports success.
  • Flow.Interview.getVariableValue only returns variables marked Available for output. Debug a formula through a copy of the flow that assigns it to an output variable.
  • Test ISBLANK first in its own IF and only reach CONTAINS with a known non-blank value. In this build BLANKVALUE did not prevent the null; nesting did.
  • Append to a multi-select by assigning existing & ";New Value" back to the field, and check for the value first so repeated runs do not duplicate it.
  • Verify a formula-driven assignment by reading the target field back, especially when it starts empty — that is exactly the case where a silent null is invisible.

Filed under

Disclosure: Doorvana's owner is also a co-owner of Nocona Consulting. This is a real engagement between two separate companies, but not an arm's-length one — and not an independent reference. More on the work page.

FAQ

Why does a Salesforce flow formula fail with "Multi-select picklist fields are only supported in certain functions"?

Flow formulas only accept a multi-select picklist field inside a small set of functions such as INCLUDES, ISBLANK and ISNULL. Concatenating one with &, or passing it to CONTAINS, is rejected at deploy or save time. The usual workaround is to copy the field into a Text variable with an Assignment element first and build the formula on the variable, then assign the result back to the field as a semicolon-separated string.

Does a Salesforce flow fault when a formula evaluates to null?

No. A formula resource that evaluates to null does not raise a fault or stop the interview. An Assignment element that uses it simply assigns null, and an Update Records element then saves that null. If the target field was already empty, nothing visibly changes, so the only way to catch it is to read the field back after the flow runs.

Can Flow.Interview.getVariableValue read a flow formula or a private variable?

No. getVariableValue only returns variables marked Available for output; for anything else it returns null, which looks identical to a variable that genuinely holds null. To inspect an intermediate value from anonymous Apex, use a debug copy of the flow that assigns the formula's result to an output variable, then read that variable.

How do you safely append a value to a multi-select picklist in a flow?

Copy the field to a Text variable, then branch with nested IF so ISBLANK is tested first: if the text is blank, assign the new value alone; only when it is known to be non-blank, use CONTAINS to check for the value and append ";Value" if it is missing. In this build, BLANKVALUE around the variable did not prevent the null result; ordering the tests so CONTAINS never sees a blank value did.

Working on something like this?

I can tell you what it takes.

If you run a home services business and this looks like a problem you have, book a call and we will talk through it.