Salesforce Scheduler — Appointment Volume Constraints Guide

Architectural options & step-by-step implementation guide for enforcing weekly capacity limits by Work Type or Service Resource.

Scope & User Experience Trade-offs

When enforcing volume limits (such as restricting a Work Type or Service Resource to maximum 3 appointments per calendar week), system architects face a core trade-off between implementation effort and user experience (UX).

Approach Validation Stage User Experience Impact Complexity
Post-Selection Validation
Before-Save Flow / Trigger
At Record Save (after slot selection) User selects slot, fills details, and clicks Save. System blocks save and throws error if cap is reached. User must go back and select another time. Low to Medium
Proactive UI Filtering
Invocable Apex + LWC / Flow
During Slot Rendering (pre-selection) Fully seamless. Fully booked slots are filtered out prior to display or marked unavailable. Database backstop ensures consistency. Medium to High
1

Option 1: Low-Code Record-Triggered Flow (Before Save)

Best for quick setup and low maintenance without custom Apex code.

How this works A Before-Save flow runs every time a Service Appointment record is created or updated. It calculates week boundaries, queries existing appointments, and blocks the save via a Custom Error element if the cap is reached.
UX Limitation Validation occurs post-selection. If the weekly limit is exceeded, the user receives an error on submission and must return to select a new slot.

Step 1 — Create Formula Resources

In Setup → Flows → New Flow (Record-Triggered Flow on ServiceAppointment), add these formula resources:

Formula 1: WeekStartDateTime

Data Type: DateTime. Calculates midnight on Sunday starting the appointment's week (UTC).

DATETIMEVALUE(
  TEXT(
    DATEVALUE({!$Record.SchedStartTime})
    - MOD(WEEKDAY(DATEVALUE({!$Record.SchedStartTime})) - 1, 7)
  ) & " 00:00:00"
)

Formula 2: WeekEndDateTime

Data Type: DateTime. Calculates midnight on the following Sunday (exclusive boundary).

DATETIMEVALUE(
  TEXT(
    DATEVALUE({!$Record.SchedStartTime})
    - MOD(WEEKDAY(DATEVALUE({!$Record.SchedStartTime})) - 1, 7) + 7
  ) & " 00:00:00"
)

Step 2 — Configure Flow Trigger & Decisions

  1. Trigger Conditions: Fast Field Updates (Before Save) when SchedStartTime Is Null = False.

  2. Get Records: Retrieve ServiceAppointment records where WorkTypeId (or Resource) matches current record, starting between {!WeekStartDateTime} and {!WeekEndDateTime}, excluding cancelled statuses.

  3. Decision & Custom Error: Count retrieved records. If count ≥ limit, route to a Custom Error element displaying a friendly message to the user.

2

Option 2: Apex Service Engine + Custom Metadata (Proactive UI & Safety Net)

Enterprise pattern combining proactive UI pre-checking for LWC/Flows with an unbreakable Apex database backstop.

Proactive UI Pre-Check + Unbreakable DB Backstop Extracting volume logic into a reusable service layer allows custom booking UIs or screen flows to pre-check slot availability (hiding capped slots) while enforcing hard constraints at the database level.

Step 1 — Create Custom Metadata Type (AppointmentConstraint__mdt)

Field LabelAPI NameTypeDescription
Work Type IdWorkTypeId__cText (18)18-char ID (Leave blank for global wildcard rule)
Service Resource IdServiceResourceId__cText (18)Optional ID for resource-level capping
Weekly LimitWeeklyLimit__cNumber (3,0)Maximum appointments allowed per week
Excluded StatusesExcludedStatuses__cText (255)Comma-separated list (e.g. Cancelled,Declined)
Timezone KeyTimezoneKey__cText (50)Java timezone ID (e.g. America/New_York)

Step 2 — Create Core Apex Service Handler

public without sharing class ServiceAppointmentConstraintHandler {

    private static final String DEFAULT_TZ       = 'America/New_York';
    private static final String DEFAULT_EXCLUDED = 'Cancelled';

    public static void validate(List<ServiceAppointment> newList, Map<Id, ServiceAppointment> oldMap) {
        Map<String, AppointmentConstraint__mdt> cfgByWorkType = loadMetadata();
        AppointmentConstraint__mdt wildcardCfg = cfgByWorkType.get('WILDCARD');

        List<ServiceAppointment> toCheck = new List<ServiceAppointment>();
        for (ServiceAppointment sa : newList) {
            if (sa.SchedStartTime == null || sa.WorkTypeId == null) continue;
            if (oldMap == null || sa.SchedStartTime != oldMap.get(sa.Id).SchedStartTime) {
                toCheck.add(sa);
            }
        }
        if (toCheck.isEmpty()) return;

        for (ServiceAppointment sa : toCheck) {
            AppointmentConstraint__mdt cfg = cfgByWorkType.containsKey(sa.WorkTypeId) ? cfgByWorkType.get(sa.WorkTypeId) : wildcardCfg;
            if (cfg == null) continue;

            Integer count = getExistingCount(sa.WorkTypeId, sa.SchedStartTime);
            if (count >= cfg.WeeklyLimit__c) {
                sa.addError('Weekly appointment cap of ' + cfg.WeeklyLimit__c + ' reached for this service.');
            }
        }
    }

    public static Integer getExistingCount(Id workTypeId, Datetime schedTime) {
        Datetime weekStart = getWeekStart(schedTime);
        Datetime weekEnd = weekStart.addDays(7);

        return [SELECT COUNT() FROM ServiceAppointment 
                WHERE WorkTypeId = :workTypeId 
                AND SchedStartTime >= :weekStart 
                AND SchedStartTime < :weekEnd 
                AND Status != 'Cancelled'];
    }

    private static Datetime getWeekStart(Datetime dt) {
        Date d = dt.dateGmt();
        Date sunday = d.toStartOfWeek();
        return Datetime.newInstanceGmt(sunday, Time.newInstance(0,0,0,0));
    }

    private static Map<String, AppointmentConstraint__mdt> loadMetadata() {
        Map<String, AppointmentConstraint__mdt> mapResult = new Map<String, AppointmentConstraint__mdt>();
        for (AppointmentConstraint__mdt c : [SELECT WorkTypeId__c, WeeklyLimit__c, ExcludedStatuses__c, TimezoneKey__c FROM AppointmentConstraint__mdt]) {
            if (String.isBlank(c.WorkTypeId__c)) mapResult.put('WILDCARD', c);
            else mapResult.put(c.WorkTypeId__c, c);
        }
        return mapResult;
    }
}

Step 3 — Create Invocable Apex Method for UI Pre-Checks

public class AppointmentConstraintInvocable {

    public class SlotCheckRequest {
        @InvocableVariable(required=true) public Id workTypeId;
        @InvocableVariable(required=true) public Datetime slotStartTime;
    }

    public class SlotCheckResult {
        @InvocableVariable public Boolean isAvailable;
        @InvocableVariable public Integer currentCount;
    }

    @InvocableMethod(label='Pre-Check Slot Volume Constraint' description='Checks whether a given slot exceeds weekly volume limits.')
    public static List<SlotCheckResult> checkSlot(List<SlotCheckRequest> requests) {
        List<SlotCheckResult> results = new List<SlotCheckResult>();
        for (SlotCheckRequest req : requests) {
            SlotCheckResult res = new SlotCheckResult();
            Integer count = ServiceAppointmentConstraintHandler.getExistingCount(req.workTypeId, req.slotStartTime);
            res.currentCount = count;
            res.isAvailable = (count < 3); // Evaluate against configured MDT limit
            results.add(res);
        }
        return results;
    }
}

Step 4 — Apex Trigger (Database Backstop)

trigger ServiceAppointmentConstraint on ServiceAppointment (before insert, before update) {
    ServiceAppointmentConstraintHandler.validate(Trigger.new, Trigger.oldMap);
}