Route Salesforce leads with round-robin using Flow Builder
Prerequisites
- Salesforce org with Flow Builder access (Enterprise, Unlimited, or Developer edition)
- System Administrator profile or "Manage Flows" permission
- Slack workspace with Salesforce for Slack app installed (for notifications)
- A list of Salesforce User IDs for your rep rotation
Why Salesforce Flow?
Flow Builder runs natively inside Salesforce with zero external dependencies — no middleware, no polling delays, no extra subscriptions. A before-save Record-Triggered Flow assigns the Lead owner before the record is even committed, which means the rep sees correct ownership from the moment the Lead appears. The trade-off is setup complexity: you need a Custom Setting for the counter, a Custom Metadata Type for the rep roster, and two separate flows (before-save for assignment, after-save for Slack).
How it works
- Custom Setting stores a persistent round-robin counter that survives across transactions without consuming SOQL queries
- Custom Metadata Type holds the rep roster (Salesforce User ID, Slack User ID, active/inactive flag) — editable without deployments
- Before-save Record-Triggered Flow fires on Lead creation, reads the counter, calculates the next rep via MOD formula, sets OwnerId, and increments the counter — all before the record commits
- After-save Record-Triggered Flow sends a Slack DM to the assigned rep with lead details (HTTP callouts aren't allowed in before-save context)
- Alternative path — Omni-Channel with a Lead Queue provides workload-based routing out of the box on Enterprise+ editions
Step 1: Create a Custom Setting for the counter
Go to Setup → Custom Settings → New.
- Label:
Round Robin Counter - API Name:
Round_Robin_Counter__c - Setting Type: Hierarchy
- Visibility: Public
Add a custom field:
- Field Label:
Last Index - API Name:
Last_Index__c - Type: Number (0 decimal places)
- Default Value: 0
After saving, click Manage and create an org-level default record with Last_Index__c = 0.
Custom Settings are accessible in flows without a SOQL query counting against governor limits. They persist across transactions, making them ideal for storing a simple counter.
Step 2: Create a Custom Metadata Type for the rep roster
Go to Setup → Custom Metadata Types → New.
- Label:
Round Robin Rep - API Name:
Round_Robin_Rep__mdt
Add these fields:
| Field | API Name | Type |
|---|---|---|
| Rep Order | Rep_Order__c | Number (0 decimals) |
| User ID | User_Id__c | Text (18 chars) |
| Slack User ID | Slack_User_Id__c | Text (15 chars) |
| Is Active | Is_Active__c | Checkbox (default: true) |
Create records for each rep in the rotation:
| Label | Rep Order | User ID | Slack User ID | Is Active |
|---|---|---|---|---|
| Alice Smith | 0 | 005xx0000012345 | U01AAAA | true |
| Bob Jones | 1 | 005xx0000023456 | U02BBBB | true |
| Carol Chen | 2 | 005xx0000034567 | U03CCCC | true |
| Dave Kim | 3 | 005xx0000045678 | U04DDDD | true |
Uncheck Is_Active__c for reps who are unavailable. The flow will skip inactive reps automatically.
Step 3: Create the before-save flow (assignment)
Go to Setup → Flows → New Flow → Record-Triggered Flow.
- Object: Lead
- Trigger: A record is created
- Optimize for: Fast Field Updates (Before Save)
Get the current counter
Add a Get Records element:
- Object:
Round_Robin_Counter__c - Store: First record →
varCounter
Get active reps
Add a Get Records element:
- Object:
Round_Robin_Rep__mdt - Filter:
Is_Active__cequalstrue - Sort:
Rep_Order__c, Ascending - Store: All records →
colReps
Calculate assignment with a Formula
Add a Formula resource:
- Name:
frmNextIndex - Type: Number
- Formula:
MOD({!varCounter.Last_Index__c}, {!colReps.Size})
This gives you the index of the next rep to assign.
Get the specific rep
Add an Assignment element to loop through the collection and pick the rep at the calculated index, or use a Loop element with a counter variable that breaks when it matches frmNextIndex.
Update the Lead owner
Add an Assignment element:
- Set:
{!$Record.OwnerId}= the selected rep'sUser_Id__c
Since this is a before-save flow, the field update happens automatically — no separate Update Records element needed.
Increment the counter
Add an Update Records element:
- Object:
Round_Robin_Counter__c - Filter: Use the record ID from
varCounter - Set:
Last_Index__c={!varCounter.Last_Index__c} + 1
You cannot send Slack messages from a before-save flow. The assignment logic must be in a before-save flow for performance (no extra DML), but the Slack notification needs a separate after-save flow or Platform Event.
Step 4: Create the after-save flow (Slack notification)
Create a second Record-Triggered Flow:
- Object: Lead
- Trigger: A record is created
- Optimize for: Actions and Related Records (After Save)
- Entry condition:
OwnerIdis not null
Send Slack notification
If you have Salesforce for Slack installed, add a Send Slack Message action:
- Slack Workspace: Select your connected workspace
- Channel or User: The assigned rep's Slack User ID (you'll need to look this up from the Custom Metadata Type using the Lead's OwnerId)
- Message: Include the lead's Name, Email, Company, and a link back to the Lead record
Alternatively, add an HTTP Callout action (available in Flow Builder as an External Service or Apex invocable):
- URL:
https://slack.com/api/chat.postMessage - Method: POST
- Headers:
Authorization: Bearer xoxb-YOUR-BOT-TOKEN - Body: Channel set to the rep's Slack User ID, with lead details in the message text
Step 5: Activate and test
- Activate both flows
- Create a test Lead manually or via the API
- Verify the Lead's OwnerId was set to the expected rep
- Check Slack for the notification DM
- Create a few more Leads and confirm the rotation cycles through all reps
If your org already has an active Lead Assignment Rule, it will run after your before-save flow. The assignment rule could overwrite the OwnerId your flow just set. Either deactivate the existing rule or add logic to your rule that preserves flow-assigned owners.
Alternative: Queue + Omni-Channel
If you're on Enterprise+ edition, Salesforce Omni-Channel provides workload-based routing out of the box:
- Create a Lead Queue and add your reps as members
- Set your assignment rule to route all Leads to the queue
- Enable Omni-Channel and configure a Routing Configuration with "Least Active" or "Most Available" routing
- Reps receive Leads in their Omni-Channel widget based on current workload
This is the officially supported path for workload-balanced routing, but it requires Enterprise edition, Omni-Channel setup, and reps to be logged into the Omni-Channel widget.
Troubleshooting
Common questions
Can I do weighted round-robin (senior reps get more leads)?
Yes. Add a Weight__c number field to the Custom Metadata Type and repeat the rep entry in the collection that many times. A rep with weight 2 appears twice in the rotation, getting double the leads. Alternatively, use a more complex formula that maps counter ranges to reps.
What happens if I deactivate a rep mid-cycle?
The MOD formula adjusts automatically to the new collection size on the next Lead creation. However, the counter value stays the same, so one rep might get skipped or doubled for a single assignment. Reset the counter to 0 after roster changes for a clean restart.
Can I route Leads to queues instead of individual users?
Yes. Store the Queue ID (starts with 00G) in the User_Id__c field of the Custom Metadata Type. Salesforce accepts queue IDs in the OwnerId field for Leads. The Slack notification step would need adjustment since queues don't have Slack User IDs — post to a channel instead.
Cost
- Flow Builder: Included in all Salesforce editions with Flow access (Enterprise+)
- Custom Settings and Custom Metadata Types: Included, no additional cost
- Omni-Channel: Included in Enterprise and Unlimited editions
Looking to scale your AI operations?
We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.