Implementing Agent Scorecard Report Templates with Configurable KPI Threshold Indicators

Implementing Agent Scorecard Report Templates with Configurable KPI Threshold Indicators

What This Guide Covers

You will construct a programmatic Agent Scorecard template using the Genesys Cloud Reporting API. The end result is a reusable scorecard definition that evaluates agents against dynamic KPI thresholds and returns structured JSON containing pass/fail indicators, ready for automated distribution or dashboard embedding.

Prerequisites, Roles & Licensing

  • Licensing: CX 3 license (required for full Reporting API access and advanced calculated metrics).
  • Roles: Administrator or a custom role with specific analytics permissions.
  • Permissions:
    • analytics:report:read
    • analytics:report:write
    • analytics:scorecard:read
    • analytics:scorecard:write
    • analytics:calculatedmetric:read
    • analytics:calculatedmetric:write
  • External Dependencies: None. This architecture relies entirely on internal Genesys Cloud reporting infrastructure.

The Implementation Deep-Dive

1. Architecting the Dynamic KPI Metrics with Data Filter Injection

Scorecards fail when thresholds are hardcoded into calculated metrics. If leadership changes the CSAT target from 85% to 90%, you cannot iterate through hundreds of agents to rebuild metrics. We must design the metric evaluation logic to accept runtime variables.

We use Data Filters to inject threshold values directly into the scorecard execution payload. The calculated metric references this filter, decoupling the business logic from the metric definition.

First, create a calculated metric that evaluates a KPI against a dynamic threshold. We use the IF function combined with a data filter reference. In the expression, {{dataFilter.csatTarget}} acts as a placeholder that the Reporting API resolves at runtime.

The Trap: Scope Leakage in Grouped Metrics
The most common architectural failure in scorecard metrics is scope leakage. When you create a calculated metric for a scorecard, the metric must evaluate within the scope of the groupBys. If you reference a global aggregate (e.g., SUM(csat.score) without a grouping context) inside a metric that is grouped by agentId, the Reporting Engine may return the global total for every agent, or it may throw a INVALID_AGGREGATION error. Always ensure that the numerator and denominator of your ratio exist within the same grouping scope. We use COUNT() and SUM() on the same dimension to guarantee the ratio calculates per agent, not globally.

Implementation:
Create the calculated metric via the API. Note the use of {{dataFilter.csatTarget}}. This tells the engine to wait for the scorecard execution payload to provide the actual threshold value.

POST /api/v2/analytics/calculatedmetrics

{
  "name": "CSAT Threshold Indicator",
  "description": "Returns 1 if agent CSAT meets the target, 0 otherwise. Threshold injected via dataFilter.",
  "expression": "IF(SUM(csat.score) / COUNT(csat.score) >= {{dataFilter.csatTarget}}, 1, 0)",
  "metricType": "number",
  "precision": 0
}

We also need the base CSAT score metric for display purposes.

POST /api/v2/analytics/calculatedmetrics

{
  "name": "Agent CSAT Percentage",
  "description": "Standard CSAT percentage for display.",
  "expression": "SUM(csat.score) / COUNT(csat.score) * 100",
  "metricType": "number",
  "precision": 2
}

2. Constructing the Scorecard Report Definition

A Genesys Scorecard is a specialized report type that inherently groups by agent and evaluates metrics over a specific date range. We construct the scorecard template using the POST /api/v2/analytics/reports/scorecard endpoint.

The reportDefinition object defines the structure. We include groupBy for agentId, the metrics we created in Step 1, and the dataFilters that hold our configurable thresholds. By defining the data filters in the template, we establish the contract for what variables the scorecard expects when it runs.

The Trap: The Null Aggregation Drop
When constructing scorecards, the Reporting Engine excludes agents who have zero interactions in the selected date range. If an agent is on leave or has no voice interactions, they do not appear in the scorecard results. This creates a “silent failure” where managers assume the agent is performing well because they are missing from the report, when in reality they simply have no data. To mitigate this, we must configure the scorecard to include agents with zero interactions if the business requires a complete roster view. We achieve this by setting the includeZeroValues flag or by ensuring the groupBy dimensions include all active agents regardless of interaction volume, though true zero-value inclusion often requires a scheduled data export merged with the WFM agent roster in an external middleware layer. For pure API scorecards, we accept the interaction-based filtering but document this behavior explicitly.

Implementation:
Create the scorecard template. We define the dataFilters with default values, but these can be overridden at runtime.

POST /api/v2/analytics/reports/schedule

{
  "reportDefinition": {
    "name": "Agent Scorecard Template - Dynamic Thresholds",
    "description": "Reusable scorecard template accepting CSAT and AHT thresholds via data filters.",
    "type": "scorecard",
    "groupBy": [
      {
        "id": "agentId",
        "type": "dimension"
      },
      {
        "id": "agentName",
        "type": "dimension"
      }
    ],
    "metrics": [
      {
        "id": "calc_agent_csat_percentage",
        "name": "CSAT %"
      },
      {
        "id": "calc_csat_threshold_indicator",
        "name": "CSAT Pass/Fail"
      },
      {
        "id": "handleTime",
        "name": "AHT (Seconds)"
      },
      {
        "id": "calc_aht_threshold_indicator",
        "name": "AHT Pass/Fail"
      }
    ],
    "dataFilters": [
      {
        "id": "csatTarget",
        "type": "number",
        "value": 85,
        "description": "Minimum CSAT percentage required to pass"
      },
      {
        "id": "ahtTarget",
        "type": "number",
        "value": 180,
        "description": "Maximum AHT in seconds allowed to pass"
      }
    ],
    "timeGroup": "day",
    "dateRange": {
      "relative": true,
      "daysAgo": 7,
      "to": "now"
    }
  },
  "schedule": {
    "recurrence": "daily",
    "time": "08:00",
    "timezone": "America/New_York"
  }
}

3. Implementing Runtime Threshold Overrides

The power of this architecture lies in the ability to override the default thresholds defined in the template when executing the scorecard. We do not modify the scorecard entity itself. Instead, we use the Reporting API to run the scorecard with a modified reportDefinition that overrides the dataFilters.

This allows a single template to serve multiple departments. The Finance team can run the template with a CSAT target of 95%, while the General Inquiries team runs the same template with a target of 80%, without requiring separate scorecard definitions.

The Trap: Filter Precedence and Date Range Conflicts
When overriding data filters, you must ensure the dateRange in the execution payload aligns with the timeGroup. If you set timeGroup to week but provide a dateRange that spans partial weeks, the Reporting Engine may truncate data or produce skewed averages because the grouping buckets do not align with the filter boundaries. Always verify that the dateRange boundaries respect the timeGroup granularity. Additionally, ensure that the dataFilters you override match the exact id strings defined in the template. A mismatch in the filter ID causes the engine to ignore the override and revert to the template default, leading to silent threshold misconfigurations.

Implementation:
Execute the scorecard with runtime overrides. We use the GET /api/v2/analytics/reports/scorecard/{scorecardId} endpoint with query parameters, or we run a one-off report using POST /api/v2/analytics/reports/scorecard with the overridden definition.

For a one-off execution with overrides:

POST /api/v2/analytics/reports/scorecard

{
  "reportDefinition": {
    "name": "Finance Team Scorecard Execution",
    "type": "scorecard",
    "groupBy": [
      {
        "id": "agentId",
        "type": "dimension"
      }
    ],
    "metrics": [
      { "id": "calc_agent_csat_percentage" },
      { "id": "calc_csat_threshold_indicator" }
    ],
    "dataFilters": [
      {
        "id": "csatTarget",
        "type": "number",
        "value": 95
      }
    ],
    "timeGroup": "day",
    "dateRange": {
      "relative": true,
      "daysAgo": 7,
      "to": "now"
    }
  }
}

The response will contain scorecardResults. Each result includes groups (agent details) and metrics. The calc_csat_threshold_indicator will return 1 or 0 based on the 95 threshold passed in the payload.

4. Automating Template Instantiation and Distribution

Manual execution is insufficient for enterprise scale. We must automate the instantiation of these scorecards for different queues or groups. We build a middleware script that retrieves the list of queues, fetches the threshold configuration from a secure key-value store (or a custom application), and triggers the scorecard API with the appropriate overrides.

The script iterates through each queue, constructs the reportDefinition with the queue-specific thresholds, and executes the scorecard. It then parses the JSON response to identify agents who failed (indicator == 0) and triggers a notification workflow (e.g., via the Genesys Engagement API or email integration).

The Trap: Rate Limiting and Concurrent Scorecard Execution
The Reporting API enforces strict rate limits. Executing scorecards for hundreds of agents or queues in rapid succession will trigger 429 Too Many Requests errors. The architecture must implement exponential backoff and queue-based execution. Furthermore, scorecard generation is computationally expensive. Running complex calculated metrics across large date ranges blocks the reporting thread. We limit the dateRange to the necessary minimum and use timeGroup aggregation to reduce the dataset size before the calculated metrics evaluate. Never run a scorecard with timeGroup: "interaction" for a 30-day range; aggregate to day or week first to prevent timeout failures.

Implementation:
Pseudocode for the orchestration logic:

import requests
import time

def run_scorecard_for_queue(queue_id, csat_target, aht_target):
    payload = {
        "reportDefinition": {
            "name": f"Queue {queue_id} Scorecard",
            "type": "scorecard",
            "groupBy": [{"id": "agentId", "type": "dimension"}],
            "metrics": [
                {"id": "calc_agent_csat_percentage"},
                {"id": "calc_csat_threshold_indicator"},
                {"id": "handleTime"},
                {"id": "calc_aht_threshold_indicator"}
            ],
            "dataFilters": [
                {"id": "csatTarget", "type": "number", "value": csat_target},
                {"id": "ahtTarget", "type": "number", "value": aht_target}
            ],
            "timeGroup": "day",
            "dateRange": {"relative": true, "daysAgo": 7, "to": "now"}
        }
    }

    response = requests.post(
        "https://api.mypurecloud.com/api/v2/analytics/reports/scorecard",
        json=payload,
        headers={"Authorization": "Bearer <OAUTH_TOKEN>"}
    )

    if response.status_code == 429:
        retry_after = int(response.headers.get("Retry-After", 5))
        time.sleep(retry_after)
        return run_scorecard_for_queue(queue_id, csat_target, aht_target)
    
    return response.json()

# Iterate queues and execute with backoff
for queue in queues:
    result = run_scorecard_for_queue(queue["id"], queue["csat_target"], queue["aht_target"])
    process_scorecard_result(result)
    time.sleep(2) # Prevent rate limiting

Validation, Edge Cases & Troubleshooting

Edge Case 1: The Phantom Agent Drop

The failure condition: Managers report that certain agents are missing from the scorecard output, leading to inaccurate performance reviews.
The root cause: The Reporting Engine excludes agents who have no interactions matching the dataFilters and dateRange. If an agent handles only chat interactions but the scorecard filters for voice metrics, or if the agent had no interactions in the selected period, they are omitted.
The solution: Validate the groupBy dimensions. If you need to include all active agents regardless of interaction volume, you cannot rely solely on the scorecard API. You must export the WFM agent roster via the WFM API and merge it with the scorecard results in your middleware, marking missing agents as “No Data” rather than omitting them entirely. Alternatively, ensure the scorecard metrics include a broad interaction type (e.g., interactionCount) so agents with any activity appear.

Edge Case 2: Threshold Rounding Discrepancies

The failure condition: An agent has a CSAT of 84.999%, and the threshold is set to 85. The indicator returns 0 (fail), but the displayed metric shows 85.0% due to UI rounding, causing confusion.
The root cause: The calculated metric evaluates using raw floating-point precision before the UI applies rounding. The IF statement compares 84.999 >= 85, which is false.
The solution: Adjust the calculated metric expression to round the value before comparison. Use ROUND(SUM(csat.score) / COUNT(csat.score), 2) >= {{dataFilter.csatTarget}}. This ensures the evaluation logic matches the displayed precision, eliminating rounding-induced failures.

Edge Case 3: Data Filter Type Mismatch

The failure condition: The scorecard runs successfully, but the threshold indicator always returns 0 or 1 regardless of the agent’s performance.
The root cause: The type of the data filter in the execution payload does not match the type expected by the calculated metric expression. For example, passing a number filter where the metric expects a percentage or vice versa, or passing a string that cannot be coerced to a number.
The solution: Verify the type field in the dataFilters array. Ensure the value passed is a valid JSON number (e.g., 85, not "85"). If the threshold is a percentage, ensure the calculated metric divides by 100 if necessary, or aligns the units correctly.

Official References