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} <> "Existing Installer", {!v_NurturingReason},
IF(ISBLANK({!v_NurturingReason}), "Existing Vendor",
IF(CONTAINS({!v_NurturingReason}, "Existing Vendor"), {!v_NurturingReason}, {!v_NurturingReason} & ";Existing Vendor")
)
)</expression>
</formulas> Read top to bottom it is three questions, each only asked if the previous one let it through:
- Is this an existing-installer outcome? If not, keep whatever is there.
- Is the current value blank? Then the answer is just
Existing Vendor. - 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
nulldoes not fault the interview. The assignment writesnulland the flow reports success. Flow.Interview.getVariableValueonly returns variables marked Available for output. Debug a formula through a copy of the flow that assigns it to an output variable.- Test
ISBLANKfirst in its ownIFand only reachCONTAINSwith a known non-blank value. In this buildBLANKVALUEdid 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
nullis invisible.