Architecting Schedule Bidding Systems Where Agents Select Preferred Shifts by Seniority

Architecting Schedule Bidding Systems Where Agents Select Preferred Shifts by Seniority

What This Guide Covers

This guide details the architectural pattern for implementing a Seniority-Based Schedule Bidding workflow within Genesys Cloud CX using WFM and custom API integrations. By the end of this implementation, you will have a system where agents submit shift preferences during a defined bidding window, and an automated process allocates those shifts based on a calculated seniority score, ensuring fairness and retention alignment while minimizing manual scheduling overhead.

Prerequisites, Roles & Licensing

  • Licensing: WFM Standard or WFM Premium license attached to the organization. WFM Premium is required if you intend to use advanced optimization features later in the lifecycle, but Standard suffices for the core bidding logic described here.
  • Permissions:
    • Wfm > Schedule > Edit (for creating the schedule template)
    • Wfm > Schedule > Publish (for locking and applying the schedule)
    • Wfm > Schedule > View (for agent access)
    • Wfm > Schedule > Bidding > Edit (if using native bidding features, though this guide focuses on a custom seniority-weighted approach)
  • API Permissions:
    • wfm:schedule:read
    • wfm:schedule:write
    • wfm:agent:read (to retrieve tenure/hire dates)
    • users:read (to resolve user IDs to names and attributes)
  • External Dependencies:
    • A reliable data source for “Seniority” metrics (typically hire_date from the User object or a custom attribute like years_of_service stored in a CRM or HRIS system).
    • A middleware environment (e.g., AWS Lambda, Azure Function, or Genesys Cloud Flows) capable of handling the sorting and allocation logic outside the native WFM UI constraints.

The Implementation Deep-Dive

1. Defining the Seniority Metric and Data Integrity

The foundation of any bidding system is the definition of “Seniority.” In many organizations, this is simply the hire date. In others, it may include role-specific tenure or performance multipliers. You must standardize this metric before touching WFM.

If you rely on the native hire_date from the User object, you must ensure this date is accurate and immutable for the duration of the bidding cycle. If you use a custom attribute (e.g., custom_attributes.seniority_score), you must build a synchronization job that updates this attribute nightly from your HRIS.

The Trap: Relying on real-time API calls to an external HRIS during the allocation phase.
The Downstream Effect: If the HRIS experiences latency or downtime during the bidding window, your allocation engine will fail or timeout. This creates a bottleneck that prevents schedule publication.
The Architectural Solution: Pre-calculate the seniority score. Create a daily cron job that fetches all active agents, calculates their seniority rank, and stores this rank in a local database or a Genesys Cloud Custom Attribute. The bidding engine should read only from this static snapshot. This decouples the scheduling process from external dependency volatility.

Code Snippet: Calculating Seniority Rank via API

You will need to fetch users and sort them. The following Python pseudocode illustrates the logic for your middleware:

import requests
from datetime import datetime

def get_seniority_ranking(base_url, access_token):
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json"
    }
    
    # Fetch all users with necessary attributes
    response = requests.get(
        f"{base_url}/api/v2/users?expand=custom_attributes",
        headers=headers
    )
    
    users = response.json().get("entities", [])
    
    # Calculate seniority based on hire_date
    # Note: Ensure hire_date is not null. Handle edge cases for new hires.
    ranked_users = []
    for user in users:
        hire_date_str = user.get("hire_date")
        if hire_date_str:
            hire_date = datetime.fromisoformat(hire_date_str.replace("Z", "+00:00"))
            # Calculate days since hire (or use a custom attribute if available)
            days_since_hire = (datetime.now() - hire_date).days
            ranked_users.append({
                "user_id": user["id"],
                "name": user["name"],
                "days_since_hire": days_since_hire,
                "custom_attributes": user.get("custom_attributes", {})
            })
        else:
            # Assign lowest priority if hire_date is missing
            ranked_users.append({
                "user_id": user["id"],
                "name": user["name"],
                "days_since_hire": 0,
                "custom_attributes": {}
            })

    # Sort by days_since_hire descending (highest tenure first)
    ranked_users.sort(key=lambda x: x["days_since_hire"], reverse=True)
    
    return ranked_users

# Usage
# ranking = get_seniority_ranking("https://api.mypurecloud.com", "your_oauth_token")

2. Configuring the WFM Schedule Template and Bidding Window

Before agents can bid, you must have a schedule template with defined shifts. In WFM, this is done via the Schedule Template.

  1. Navigate to Workforce Management > Schedules > Schedule Templates.
  2. Create a new template or duplicate an existing one.
  3. Define the Time Off Requests and Shift Definitions. Ensure that each shift has a unique shift_id or is identifiable by start_time, end_time, and date.
  4. Enable Bidding in the schedule settings if your license allows, but note that native WFM bidding is often “first-come, first-served” or based on simple priority groups. For true seniority-based allocation across a complex matrix, you will likely need to disable native bidding and use the schedule as a “request” mechanism, processing the allocation externally.

The Trap: Using Native WFM Priority Groups for Seniority.
The Downstream Effect: Native priority groups are static. You cannot dynamically reorder agents within a group based on a calculated score without manual intervention every week. If Agent A (5 years tenure) and Agent B (4 years tenure) are in the same group, the system cannot distinguish between them during allocation. This leads to arbitrary assignment, causing employee dissatisfaction.
The Architectural Solution: Use the WFM Schedule as a repository of available shifts and agent preferences, but perform the matching logic in your middleware. Agents submit preferences via a custom portal or a Genesys Flow that writes to a custom attribute or an external database. The middleware then matches these preferences against the seniority ranking.

3. Building the Agent Preference Capture Mechanism

Agents need a way to submit their preferences. You have three primary options:

  1. Genesys Cloud Flows: Create a Flow that agents access via the Agent Desktop or a self-service web portal. The Flow collects their top 3 shift preferences for the upcoming week.
  2. Custom Web Portal: Build a React/Angular app that authenticates via Genesys OAuth and allows agents to drag-and-drop shifts.
  3. Email/CSV Import: A low-tech approach where agents submit a CSV file, which is parsed by your middleware.

For this guide, we assume a Genesys Flow integration for tight ecosystem coupling.

Flow Design Steps:

  1. Start Element: Triggered by Agent Desktop or IVR.
  2. Get User: Retrieve the current agent’s ID.
  3. Get Schedule Data: Call the WFM API to retrieve available shifts for the upcoming planning period.
    • Endpoint: GET /api/v2/wfm/schedules/{scheduleId}/shifts
  4. Display Shifts: Use a Menu or Form element to present available shifts.
  5. Collect Preferences: Allow the agent to select up to 3 preferences.
  6. Store Preferences: Use a Set Variable action to store the preferences in a custom attribute on the User object (e.g., custom_attributes.bidding_preferences_week_42).
    • Format: {"1": "shift_id_123", "2": "shift_id_456", "3": "shift_id_789"}

The Trap: Storing preferences in a non-structured format.
The Downstream Effect: If you store preferences as a free-text string, parsing becomes error-prone. If you store them in separate custom attributes for each shift (e.g., pref_1, pref_2), you limit scalability.
The Architectural Solution: Store preferences as a JSON string in a single custom attribute. This allows your middleware to deserialize the entire preference set in one call. Ensure the custom attribute is defined as a String type in Genesys, not a Number or Date.

4. The Allocation Engine: Matching Preferences to Seniority

This is the core logic that runs after the bidding window closes. Your middleware must execute the following steps:

  1. Ingest Data:
    • Fetch the seniority ranking (from Step 1).
    • Fetch all agent preferences (from User custom attributes).
    • Fetch all available shifts (from WFM API).
  2. Initialize Allocation State:
    • Create a map of shift_id to assigned_agent_id. Initially, all shifts are unassigned.
    • Create a set of assigned_agents to prevent double-booking.
  3. Iterate Through Seniority List:
    • Loop through agents from highest seniority to lowest.
    • For each agent, check their preferences in order (1st choice, then 2nd, then 3rd).
    • If the preferred shift is unassigned, assign it to the agent.
    • Mark the agent as assigned and break the inner loop (move to the next agent).
  4. Handle Unassigned Agents/Shifts:
    • After processing all preferences, you may have unassigned agents or unassigned shifts.
    • Implement a fallback logic: Assign remaining agents to remaining shifts based on availability (e.g., nearest to their preferred time) or leave them unassigned for manual review.

Code Snippet: Allocation Logic (Python)

def allocate_shifts(seniority_ranking, agent_preferences, available_shifts):
    # Initialize data structures
    shift_assignments = {}  # shift_id -> agent_id
    agent_assignments = {}  # agent_id -> shift_id
    
    # Map shift IDs for quick lookup
    available_shift_ids = set(s['id'] for s in available_shifts)
    
    for agent in seniority_ranking:
        agent_id = agent['user_id']
        prefs = agent_preferences.get(agent_id, [])
        
        # If agent has no preferences, skip to fallback logic later
        if not prefs:
            continue
            
        assigned = False
        for pref_shift_id in prefs:
            if pref_shift_id in available_shift_ids:
                # Assign the shift
                shift_assignments[pref_shift_id] = agent_id
                agent_assignments[agent_id] = pref_shift_id
                available_shift_ids.remove(pref_shift_id)
                assigned = True
                break
        
        # If not assigned by preference, mark for fallback
        if not assigned:
            pass # Handle in fallback phase

    # Fallback Logic: Assign remaining agents to remaining shifts
    unassigned_agents = [a for a in seniority_ranking if a['user_id'] not in agent_assignments]
    remaining_shifts = [s for s in available_shifts if s['id'] not in shift_assignments]
    
    # Simple round-robin or availability-based assignment here
    for i, agent in enumerate(unassigned_agents):
        if i < len(remaining_shifts):
            shift = remaining_shifts[i]
            shift_assignments[shift['id']] = agent['user_id']
            agent_assignments[agent['user_id']] = shift['id']
            
    return shift_assignments, agent_assignments

5. Writing the Schedule Back to WFM

Once the allocation is complete, you must write the schedule back to Genesys Cloud WFM so it appears in the Agent Desktop and WFM UI.

  1. Create a New Schedule Version:
    • Endpoint: POST /api/v2/wfm/schedules
    • Body: Include the name, description, and state (e.g., DRAFT).
  2. Add Shifts to the Schedule:
    • Endpoint: POST /api/v2/wfm/schedules/{scheduleId}/shifts
    • Body: Include the agent_id, shift_id (or start_time/end_time if creating new shifts), and date.
    • Critical: Ensure you include the location_id if your organization uses multiple locations.
  3. Publish the Schedule:
    • Endpoint: POST /api/v2/wfm/schedules/{scheduleId}/actions/publish
    • This locks the schedule and makes it visible to agents.

The Trap: Publishing a schedule with conflicting shifts.
The Downstream Effect: If your allocation logic has a bug and assigns two shifts to the same agent on the same day, WFM will reject the publish action with a CONFLICT error. This halts the entire process.
The Architectural Solution: Implement a pre-validation step in your middleware. Before calling the WFM API, check for overlapping shifts for each agent. If a conflict is detected, log an error and halt the publish process, alerting the WFM administrator for manual intervention.

Code Snippet: Publishing Schedule via API

def publish_schedule(base_url, access_token, schedule_id, shifts):
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json"
    }
    
    # 1. Add shifts to the draft schedule
    for shift in shifts:
        response = requests.post(
            f"{base_url}/api/v2/wfm/schedules/{schedule_id}/shifts",
            headers=headers,
            json=shift
        )
        if response.status_code != 201:
            raise Exception(f"Failed to add shift: {response.text}")
            
    # 2. Publish the schedule
    response = requests.post(
        f"{base_url}/api/v2/wfm/schedules/{schedule_id}/actions/publish",
        headers=headers
    )
    
    if response.status_code == 200:
        print("Schedule published successfully.")
    else:
        raise Exception(f"Failed to publish schedule: {response.text}")

# Usage
# publish_schedule("https://api.mypurecloud.com", "your_oauth_token", "schedule_123", [shift1, shift2])

Validation, Edge Cases & Troubleshooting

Edge Case 1: The “Tie-Breaker” Problem

The Failure Condition: Two agents have identical seniority (e.g., hired on the same day) and both bid for the same shift.
The Root Cause: The sorting algorithm is stable, but if the seniority metric is identical, the order is arbitrary. This can lead to perceived unfairness if Agent A gets the shift one week and Agent B the next, without a clear rule.
The Solution: Implement a secondary tie-breaker. Use the user_id (alphabetical order) or a random seed generated at the start of the bidding cycle. Document this tie-breaker rule in the agent handbook to manage expectations.

Edge Case 2: Partial Preference Submission

The Failure Condition: An agent submits only one preference, and it is taken by a more senior agent. The agent is left unassigned.
The Root Cause: The allocation engine only looks at submitted preferences. If an agent does not submit a 2nd or 3rd choice, the engine cannot assign them a fallback shift.
The Solution: Enforce a minimum number of preferences in the Flow/UI. Require agents to submit at least 3 preferences. If they refuse, do not include them in the automatic allocation and flag them for manual scheduling by the WFM team.

Edge Case 3: Shift Availability Mismatch

The Failure Condition: The number of available shifts is less than the number of bidding agents.
The Root Cause: Understaffing or inaccurate headcount planning.
The Solution: Before running the allocation, compare the count of available shifts against the count of bidding agents. If there are more agents than shifts, notify the WFM administrator immediately. The system should halt and request manual intervention to add shifts or remove agents from the bidding pool.

Edge Case 4: API Rate Limiting

The Failure Condition: The allocation engine hits Genesys Cloud API rate limits when fetching user data or writing shifts.
The Root Cause: High concurrency or inefficient polling.
The Solution: Implement exponential backoff in your API client. Use batch operations where possible (e.g., PATCH /api/v2/users for bulk updates). Schedule the allocation job during off-peak hours to reduce contention.

Official References