[object Object]

Omnichannel for Customer Service routes work items using a workflow that matches required skills against agent skills. Each skill has a proficiency score. On day one this is precise. On day 400, every senior agent has Level 5 on every skill, every junior has Level 3 on most, and the routing engine cannot distinguish anyone. The platform treats skills as static. They are not.

How routing actually picks an agent

The routing workflow evaluates a queue’s classification rules, scores agents against required skills, then ranks. Ranking considers:

  1. Agent presence — only Available agents are candidates.
  2. Capacity — agents at their unit ceiling are excluded.
  3. Skill match — proficiency score per matched skill, summed or maxed depending on config.
  4. Longest idle, optionally.

The skill match step is where decay matters. If three agents all match Level 5 on the required skill, the engine picks by capacity and idle time. The 95th-percentile agent on real performance gets the same volume as the 50th-percentile.

Why skills become stale

  • Skills are awarded manually or via a one-time training event. Nothing decrements them.
  • Agents change teams, but skill records persist.
  • Proficiency was inflated at onboarding to expand the candidate pool when the team was understaffed.
  • New product lines launch, get a skill, get assigned to everyone for “coverage”, and never get re-scored.

Without intervention, the skill matrix becomes a flat-5 grid within a year.

The decay model

Tie skill proficiency to recent outcomes. Each closed work item produces a quality signal: CSAT, resolution time vs target, escalation flag. A nightly job re-scores skills using an exponential moving average:

new_proficiency = alpha * recent_score + (1 - alpha) * old_proficiency

Where recent_score is the average outcome on work items requiring that skill in the last N days, scaled to 1-5. Alpha around 0.15 gives meaningful drift over a quarter without thrash. Skills with no recent work items decay slowly toward 3 (the population mean), not zero — an agent with no recent chats on “Returns” is not necessarily worse, just stale.

Implementing it

The agent-skill link is bookableresourcecharacteristic. The scoring loop:

// Pseudocode for nightly scoring batch
const agents = await query(`
  SELECT systemuserid, fullname FROM systemuser
  WHERE applicationid IS NULL AND isdisabled = 0
`);

for (const agent of agents) {
  const skills = await getAgentSkills(agent.systemuserid);
  for (const skill of skills) {
    const recent = await getRecentOutcomes(
      agent.systemuserid, skill.characteristicid, 30
    );
    if (recent.count === 0) {
      const decayed = drift(skill.ratingvalue, 3, 0.05);
      await updateRating(skill.bookableresourcecharacteristicid, decayed);
      continue;
    }
    const score = composite(recent);
    const next = 0.15 * score + 0.85 * skill.ratingvalue;
    await updateRating(
      skill.bookableresourcecharacteristicid,
      clamp(next, 1, 5)
    );
  }
}

function composite(outcomes) {
  // CSAT (1-5), resolution speed (1-5), escalation penalty
  return outcomes.csat * 0.5
       + outcomes.speed * 0.4
       - outcomes.escalation_rate * 1.0;
}

Run it nightly via an Azure Function. Write changes through the Dataverse Web API. Log the deltas to a custom table so supervisors can see why an agent’s score moved.

The political problem

You will be wrong about ten percent of the changes. An agent who took two bad days off will see their score drop. A new product launch will tank everyone’s “speed” score on that skill until the team learns it. Surface these as proposals to a supervisor, not auto-applied. The supervisor review queue is a Power Automate flow that picks up deltas above a threshold, posts to Teams, and waits for approve/reject.

trigger: When delta_score > 0.5 OR delta_score < -0.5
actions:
  - post_to_teams:
      channel: routing-skill-changes
      card:
        title: Proposed skill update
        body: "Agent {agent}: {skill} {old} -> {new}"
        buttons: [Approve, Reject]
  - on_approve: WriteToDataverse
  - on_reject: LogAndDiscard
  - on_timeout_24h: WriteToDataverse  # default approve

Routing logic side

The routing workflow itself stays simple. Match required skills, rank by proficiency. The intelligence is in keeping proficiency honest. If you complicate the routing rules with composite expressions, you make the system opaque and hard to audit. Decay model in the data, simple rules in routing.

What this fixes

After three months of running this, our top-quartile agents take 22% more work items in their strongest skill area. Junior agents see more diversified routing — they stop getting flooded with topics they were nominally Level 4 in but actually struggled with.

What this does not fix

  • Capacity ceilings. If your best agent caps at five concurrent chats, no amount of scoring fixes the pipe.
  • The Omnichannel UI does not show proficiency to agents. They see “you got assigned this” without context.
  • Cross-skill correlation. Real agents are better at related topics; the model treats each skill independently. Add a clustering layer if you care.

Pixel notes

Build a supervisor view showing each skill as a column, each agent as a row, with cell color reflecting proficiency. Add a tooltip showing trend over 90 days. This view is the forcing function for skill reviews. Without it, the supervisor never opens the agent-skill table by hand.

See also

Customer service routing intersects with capacity profiles — capacity rules constrain the routing decision before skills ever get evaluated.

Bottom line

  • Static skill tables flatten to all-5 within a year.
  • Tie proficiency to recent outcomes via a nightly EMA.
  • Drift unused skills toward the population mean, not zero.
  • Surface changes for supervisor approval; do not auto-apply silently.
  • Keep routing rules simple; put intelligence in the data.
[object Object]
Share