Salesforce Flow and Product Schedules









One of the very first Salesforce projects I worked involved using Product Schedules. Amongst the client’s requirements was to have the schedule dates move automatically if the Close Date of the Opportunity changed. This couldn’t be done by configuration alone, so I had to ask a friendly Developer to create some code (thanks, Inês Costa!).


But now, using Workflow and Flow, there is a configuration option. This post is to show how you can tackle this requirement today with clicks only. It is a great example of how Flow is changing the life of the Salesforce Administrator.

What are Product Schedules?

Companies selling products where the revenue is recognised over time should be using Product Schedules in Salesforce. Details of implementing Product Schedules can be found here:

When Dates Move

When a Product (with a schedule set up) is added to an Opportunity, Salesforce automatically sets up Product Schedule records. These are records that have a date, quantity and revenue. The dates are based on the Opportunity Line Item’s Date or – if that is blank – the Opportunity’s Close Date. If the Close Date subsequently shifts (as it might well do!), the Product Schedule records DO NOT automatically change to reflect the new date. To correct the dates, Salesforce expects the user to go into each Opportunity Line Item and click on the “Re-Establish” button to re-calculate the schedule dates. Naturally, this does not always happen, so the schedule dates no longer reflect reality. Reports and dashboards will show wrong dates. Management will start to lose faith with the data.

Flow Solution




The solution described here uses a Custom Field, two Flows a Workflow and a Process (Sometimes called a Lightning Process). I’ve written it as a set of instructions (with screen shots) and added commentary on why certain things are done and how it all knits together.

Custom Field

In the Opportunity object, create a number field called “Days Shifted”. This is to capture the number of days the Close Date moves by when it is edited. Set the default value to be zero. The field should not be added to any page layouts as it is just being used it in the background. Remember to add a description to the field when creating it, so a future admin knows what it’s there for!

Workflow 1 – Populating Days Shifted

This simple workflow fires whenever the Close Date is changed:








The workflow action is to set the field value of Days Shifted:








Make sure that “Re-evaluate Workflow Rules After Field Change” is ticked!

Flow 1 – Update Line Schedules

As mentioned, there will be two flows. This flow will be a sub-flow in Flow 2. So at the moment, some elements of it will seem to be loose ends.

What this flow does is find all the Product Schedule records associated to an Opportunity Line record and update all the dates on those records.

[Side note – when I say “Product Schedule”, I technically mean “Opportunity Line Item Schedule”. But “Product Schedule” is shorter and more widely used!]


Here’s what is going on:

  1. Looks up all Product Schedule records that relate to an Opportunity Line and assigns them to a SObject Collection Variable.
  2. Assigns the first record in that collection to a SObject Variable and then passes that record onto…
  3. Assigns the new Date value (calculated in a formula in the Flow – see below) to that record.
  4. Adds that record to a second SObject Collection Variable.
  5. Updates all the records using the new collection’s values.




If you are new to Flow (sometimes also called Visual Flow), a lot of that may sound like double-dutch. Below is a brief explanation of each element. If you are completely new to Flows, then the Salesforce guide is here:

Other resources are available!

Flow Variables and Formula

The following variables and formula in the flow were created from the “Resources” tab:

varDaysShiftNumberWill ultimately come from the custom field created earlier.
scvLineSchedulesSObject Collection VariableDefined as part of step 1. Object is OpportunityLineItemSchedule
scvNewLineSchedulesSObject Collection VariableComes into play in steps 4 and 5. Object is OpportunityLineItemSchedule
flaNewScheduleDateFormula (Date){!svarLineSchedule.ScheduleDate} + {!varDaysShift}
svarLineScheduleSObject VariableSet in step 2. Object is OpportunityLineItemSchedule
svarOpportunityLineItemSObject VariableComes into play in the next Flow. Just create it for now Object is OpportunityLineItem.

Brief Overview of the Different Variables

Flow can be used to either deal with a single record (say an Opportunity record) or multiple records all at once – a collection (e.g. all the Opportunity records relating to an Account). Different types of variable are used depending on which scenario you are in. Are you dealing with one record or many at once?

Variable is the value of a specific field in that one record (e.g. the Close Date of an Opportunity).

SObject Variable is a single record, but holds multiple field values of that record in it (e.g. an Opportunity with its Close Date, Account ID, Stage and Amount).

SObject Collection Variable is a group of records that have been pulled into or created within the flow (e.g. all Opportunities relating to an Account).


Step 1 – Fast Lookup

Fast Lookups are a way of grabbing multiple records and adding them to a collection:










This is telling the flow to find all OpportunityLineItemSchedule records (Product Schedule records) where the Opportunity Line Item ID is the same as the svarOpportunityLineItem.Id. (svarOpportunityLineItem.Id will be set in the other flow.)

All qualifying records are put into a collection (scvLineSchedules) and the ScheduleDate of these records is stored (with the record IDs) in the collection.

Step 2 – The Loop

The Loop is used to cycle through a collection of records.







Here, the loop is set to cycle through all the records in the scvLineSchedules collection and assign the next record to the SObject variable svarLineSchedule.

When drawing a connection from a loop, it gives two options: “for each value in the collection” and “when there are no more values to process”.


Step 3 – Assigning the new Date to the Record

The loop has put a single record to svarLineSchedule. The next step is to assign the new date value to that record. The formula needed was set up earlier as flaNewScheduleDate.







The truncated text in the Variable box is “{!svarLineSchedule.ScheduleDate}”. To get to the specific field in a SObject variable, click on the small black triangle next to the variable’s name.


Step 4 – Add the updated record to a new Collection

Now that the SObject variable has been updated with the new date value, it needs to be added to a new collection within the flow.






Note that the Operator is “add”. The record is being added to a collection.


The flow now points back to the loop, where the next record will be picked, the date updated and the result added to the new collection. This continues until all the records in the collection have been processed.

Step 5 – Update Salesforce

This last, simple stage is where the values in the new collection (scvNewLineSchedules) are used to update all the records in Salesforce.







Don’t forget to save the flow! In this instance, the flow has been called “flowUpdateLineSchedules”.


Flow 2 – Updating Opportunity Line Item Dates

The first flow loops through all the Product Schedule records and updates the date on each. But Product Schedules are against the Opportunity Line Items, not the Opportunity. This flow loops through the Opportunity Line Item records associated to an Opportunity and uses the first flow as a sub-flow:


  1. Looks up the Opportunity and uses the value in the Days Shifted field to populate a variable (see table below).
  2. Looks up all Opportunity Line Item records that relate to the Opportunity and assigns them to a SObject Collection Variable.
  3. Assigns the first record in that collection to a SObject Variable and then passes that record onto…
  4. Flow 1 (now you see why I created Flow 1 first!)
  5. Assigns the new Date value (calculated in a formula in the Flow – see below) to the Opportunity Line Item record.
  6. Adds that record to a second SObject Collection Variable.
  7. Updates all the records using the new collection’s values.
  8. Sets the Days Shifted value back to 0 (ready for next time).



Hopefully, you can see that there are a lot of similarities between this flow and the first. The only real differences are elements 1, 4 and 8. Also, this time it is looping through Opportunity Line Item records and not Product Schedule records.


Flow Variables and Formula

varDaysShiftNumberIs set by the Workflow that will launch this flow. Is passed on to the sub-flow in step 4.
varOpportunityIDVariable (Text)Is set by the Workflow that will launch this flow
scvOpportunityLinesSObject Collection VariableDefined as part of step 2
scvNewOpportunityLinesSObject Collection VariableComes into play in steps 6 and 7
flaNewDateFormula (Date){!svarLineSchedule.ScheduleDate} + {!varDaysShift}
svarOpportunityLineSObject VariableSet in step 3. Is passed on to the sub-flow in step 4.


Step 1 – Lookup Opportunity

This is a simple lookup to pull information from a single record – the Opportunity that has had its Close Date changed. This lookup finds the record in question and uses to the value in “Days Shifted” to populate the variable in the flow.




Steps 2, 3, 5, 6 and 7

These are virtually identical to steps 2, 3, 4 and 5 in the first flow, so please refer to the above sections.


Step 4 – Calling Flow 1

To put in a sub-flow, Salesforce needs to know which variables it is setting in the target flow (flowUpdateLineSchedules) and what values to set. Here the target sub-flow is identified and the two values required in the sub-flow set:








The values required for the sub-flow to work are svarOpportunityLineItem and varDaysShifted (i.e. which Opportunity Line Item record is being updated and what value is to be used in flaNewScheduleDate).


Flow insists that at least 1 output value is set, even though it is not necessary from a data point of view here. So a variable is set to be passed back.




Step 8 – Resetting the Days Shifted to zero

This is a simple single record update to change the value stored in Days_Shifted__c back to 0.







We need to do this in preparation for the next time the Close Date is modified.

Process – Firing Flow 2

To fire the Flow when the Date changes, a Process is needed. Processes are relatively new to Salesforce – they’re souped up Workflows that give us Admins many more options. In this case, a very simple Process will suffice.

Note that the ISCHANGED function is not available in Process criteria, hence we reset the value of “Days Shifted” back to zero in the second Flow.

From the “Create Process” (found very near the link to create a new Flow), give the Process a name and specify the object that the Process relates to. In this case, it’ll be Opportunity (as it is a change to an Opportunity record that is to set everything in motion).

Then set the Criteria that are to be evaluated. So an Opportunity where the Days Shifted value is not zero:







Under Immediate Actions, tell the Process to launch the Flow (Flow 2 – here called “flowRedateOpportunityItems”):







Tell the Process Action to set varOpportunityID in the flow as the ID of the Opportunity record and save your Process.

Once saved, Click on the “Activate” button (top-right) to turn the Process on.


Final Configuration

Now the solution is built, all that is left is to do some testing. So Activate the Flows, Workflow and Process and test away!


Points for Consideration

Opportunity Line Item Edits

A user can manually change the dates on an individual Opportunity Line Item record. The solution above only looks at changes made to the Opportunity Close Date. But you can see how you could set up a custom field and workflows on Opportunity Line Item and feed values directly into the first flow.

Call Limit

If you’re new to flows, you might be wandering what this collection stuff is about. Basically, it’s to do with governor limits in Salesforce. Every time a flow pulls data out of Salesforce or writes information into Salesforce, it counts as an API call. So if you’re handling 1 record at a time (rather than collections), those API calls start to rack up quite quickly. The daily limit is 250,000 calls (or 200 x licenced users if you happen to be on an org with more than 1,250 users). Although that sounds like a large number, it can easily be hit if updating each record one-by-one.

Batch Limit

Another limit that is worth noting is the batch size. A collection cannot contain more than 200 records. That is not likely to be an issue here, but could be if you have a Product Schedule set up with daily revenues over a year. In such cases, you’ll need to be a little more creative in how you are defining the collections.

Mass Updates

Any mass change of Close Dates using a data loader could easily blow the batch limit mentioned above. Changing the chunking settings (the number of records that the data loader tries to process at a time) down to 1 (normal default is 200) may work. This will significantly increase the time it takes to perform the mass update.

Change Sets

At present, Flow Triggers cannot be added to a change set. Flows can be added, but must be activated after being imported as part of an inbound change set.


So there we have it. Two Flows, a Workflow, a Process and a Custom Field. And no code! The whole thing took me about 90 minutes to build.

This makes Product Schedules a much more user-friendly tool and greatly improves forecasting accuracy.


This is just one potential application of Flow triggered by Workflow. If you’ve enhanced a Salesforce process by using Flow, please share your experiences with me.


Original Article By: Nick Spencer.


8 thoughts on “Salesforce Flow and Product Schedules

  • August 10, 2017 at 1:57 pm

    This is a nice post and could be helpful to me. However, the table under Flow Variable and Formulas section is not shown. As a result, there are some critical details that are missing. Would it be possible to obtain that table from you?



    • August 10, 2017 at 2:19 pm

      Hi Adam,

      Sorry about that, the table plugin I use needed updating, you should see it now.


  • February 6, 2019 at 1:17 pm

    I’ve spent a couple of days trying to build this, but it’s not working and I cannot spot where I have gone wrong. I have run a debug (see below) and have a few questions:

    1) I’ve set all inputs and outputs to private except for the following: MAIN FLOW: varDaysShift = input and output, varOpportunityID = input only, svarLineItems input = only and SUBFLOW: varDaysShift = input and output and svarOpportunityLineItem = input only. Is this correct / does this matter?

    2) I think either the list of variables for the main flow not complete or more likely I am misunderstanding something. As in the main flow I had to create an additional variable (svarLineSchedule) in order to calculate the formula on flaNewDate (i.e. {!svarLineSchedule.ScheduleDate}+{!varDaysShift} ) As it is not possible to look up the schedule date from the svarLineItems variable.

    3) Also, in the main flow on the fastlookup I have stated Lookup OpportunityLineItem where OpportunityId = {!varOpportunityID} (the text variable set up for the record lookup in step 1) is this correct or should it be OpportunityID = {svarOpportuntiiy} (i.e. I need to create an additional SObject Variable on the opp which is not mentioned in the table for the main flow).

    4) Maybe most important – can you spot what I’ve done wrong from the debug log??
    Here’s the log:

    How the Interview Started
    Theodore Ray (0050W000006oqCt) started the flow interview.
    Some of this flow’s variables were set when the interview started.
    varOpportunityID = 0060S000007FpAH
    varDaysShift = 65
    RECORD QUERY: lookupOpportunity
    Find one Opportunity record where:
    Id Equals {!varOpportunityID} (0060S000007FpAH)
    Successfully found record.
    {!varDaysShift} = 0
    FAST LOOKUP: flOpportunityLineItems
    Find all OpportunityLineItem records where:
    OpportunityId Equals {!varOpportunityID} (0060S000007FpAH)
    Assign those records to {!scvLineItems}.
    Save these field values in the variable: ServiceDate
    Successfully found records.
    LOOP: loopOpportunityLines
    Loop Through: [00k0S0000058rp5QAA]
    Iteration: 0
    Current value of {!svarLineItems}: 00k0S0000058rp5QAA
    SUBFLOW: flowUpdateLineSchedules
    Enter flow UpdateLineSchedulesCloudFlowDesigner version 4.
    varDaysShift = {!varDaysShift} (0)
    svarOpportunityLineItem = {!svarLineItems} (OpportunityLineItem (00k0S0000058rp5QAA))
    FAST LOOKUP: flOpportunitySchedules
    Find all OpportunityLineItemSchedule records where:
    Id Equals {!svarLineSchedule.Id} (null)
    Assign those records to {!scvLineSchedules}.
    Save these field values in the variable: ScheduleDate
    Failed to find records.
    LOOP: loopLineSchedules
    End Loop.
    FAST UPDATE: fuLineItemSchedules
    Update OpportunityLineItemSchedule records whose IDs are stored in {!scvNewLineSchedules}.
    Variable Values
    All records whose IDs are in {!scvNewLineSchedules} are ready to be updated when the next Screen or Wait element is executed or when the interview finishes.
    SUBFLOW: flowUpdateLineSchedules
    Exit flow UpdateLineSchedulesCloudFlowDesigner version 4.
    {!varDaysShift} = varDaysShift (0)
    ASSIGNMENT: assLineDate
    {!svarLineItems.ServiceDate} Equals {!flaNewDate}
    {!svarLineItems.ServiceDate} = “null”
    ASSIGNMENT: assNewOpportunityLines
    {!scvNewOpportunityLines} Add {!svarLineItems}
    {!scvNewOpportunityLines} = “[OpportunityLineItem (00k0S0000058rp5QAA)]”
    LOOP: loopOpportunityLines
    End Loop.
    FAST UPDATE: fuOpportunityLines
    Update OpportunityLineItem records whose IDs are stored in {!scvNewOpportunityLines}.
    Variable Values
    All records whose IDs are in {!scvNewOpportunityLines} are ready to be updated when the next Screen or Wait element is executed or when the interview finishes.
    RECORD UPDATE: updateOpportunityDaysShifted
    Find all Opportunity records where:
    Id Equals {!varOpportunityID} (0060S000007FpAH)
    Update the records’ field values.
    Set_Days_Shifted__c = 0
    All records that meet the filter criteria are ready to be updated when the next Screen or Wait element is executed

  • February 6, 2019 at 4:32 pm

    Actually – have now made it work – the debug log in really useful – I just needed to use it better!

    • February 6, 2019 at 4:34 pm

      Perfect. Are there any changes that I need to make?

      • February 6, 2019 at 6:01 pm

        Hi Darren. First – thank you so much for taking the time to publish this really useful guide and for getting back to me! For me as a near total newbie to flows it would have been a bonus to have a screen shot for each of the master flow steps even when they are very similar to the sub-flow steps as this removed the element of guess work / doubt from the replication process. Also, it would have been a double bonus to explicitly be told which of the options to select for the loop to assignment link – I’m sure it’s blindingly obvious to someone with a passing knowledge of flows but it was less so to me…
        Finally, I made a small enhancement to the workflow functionality which I think works well for our org and might for others too. That is that in general close won dates are not the final arbiter of when the revenue schedules should start. We have a contract start date which should be used when populated. For this reason the workflow I used fires on:

        ISCHANGED( Contract_Start_Date__c )

        and has a field update which states:

        !ISBLANK( Contract_Start_Date__c ) ,
        ISBLANK(PRIORVALUE( Contract_Start_Date__c )),
        CloseDate > Contract_Start_Date__c
        CloseDate – Contract_Start_Date__c ,

        !ISBLANK( Contract_Start_Date__c ) ,
        ISBLANK(PRIORVALUE( Contract_Start_Date__c )),
        CloseDate < Contract_Start_Date__c
        Contract_Start_Date__c – CloseDate,

        !ISBLANK( Contract_Start_Date__c ) ,
        !ISBLANK(PRIORVALUE( Contract_Start_Date__c ))
        Contract_Start_Date__c – PRIORVALUE(Contract_Start_Date__c),
        CloseDate – PRIORVALUE(CloseDate)

        • February 7, 2019 at 1:04 pm

          Thanks for you’re reply, and glad you found my site useful; my aim for 2019 is to blog more.
          Thank you for the workflow suggestion. I think I’m going to try and redo this and add more detail as you suggest.


Leave a Reply

Your email address will not be published. Required fields are marked *