← All work
Case Study / Zoom / Protective Design

Managing a workforce of thousands

Increasing transparency and control in Zoom's workforce management product — designing features that act on people in bulk.

The Zoom WFM Schedules screen: a weekly staffing grid with each agent as a row and the day laid out in columns from 8am to 8pm, filled with colored activity blocks for productive time, meals, breaks, meetings, and training. The left nav lists Adherence, Schedules, Schedule configuration, Scheduling groups, Forecasting, Request management, Preferences, and more.
The WFM schedule — every agent a row, the day across the top, activity blocks for who's doing what. This is the surface a supervisor works in, and where most of these features act.
Company

Zoom

Year

2024

Role

Product designer

Overview

The problem

Zoom's Workforce Management product is the operational backbone of a contact center. It forecasts how much volume is coming, builds the schedules to cover it, tracks whether agents are where the schedule says, and manages their time off — for a workforce that runs from a few hundred people to a few thousand.

Nearly everything a supervisor does in it is a bulk action. They don't schedule an agent, they schedule a group. They don't move a break, they re-optimize an afternoon. That's the whole point of the product, and it's also the risk: an action that goes wrong goes wrong for hundreds of people simultaneously, and the people it happened to aren't in the room.

I was the only designer on WFM for ten months, working across most of its surface.

The opportunity

The reframe

A run of features is not a story. It's a backlog — notes on activities, an HR integration, a rotational template, a reporting surface, a favorites list. They arrived one ticket at a time from a PM who knew the customers well, and I worked each one from the ticket into user framing, jobs, and priority before designing it.

What I noticed partway through is that they weren't as different as they looked. Every one of them was a system doing something on a person's behalf — placing an activity on their day, moving their lunch, approving their time off, adjusting a balance they'd never see. And the interesting design question in each was almost never how to perform the action. It was what the system owed the people it had just acted on, and the supervisor accountable for them.

So the work stopped being a backlog and became one standard applied across all of it.

Scheduling is where the system touched people most directly, so that's where the work concentrated — and where the standard got its hardest tests.

Approach

How I approached it

Four principles, drawn out of the work rather than set down in front of it:

  • Bound what the automation may doIf a system is going to rearrange someone's day, the supervisor decides how far it may move things and whose schedules are in scope.
  • Show the change before committing to itPreview by default. Applying is a separate decision from calculating.
  • Name who got left out, and whyA count is not an answer. A categorized reason that opens to names is.
  • Tell the person whose day changedDisclosure that stops at the supervisor is only half of it.
Bulk scheduling

Scheduling an activity across hundreds of agents

A supervisor scheduling a training session isn't scheduling one person. They're putting an activity on two hundred schedules, and some of those agents are already booked — a 1:1, a lunch, a day off. The design question was what the system owes the supervisor about the agents it can't place.

The feature splits at that question. Fixed is for activities that have to happen at a set time — the trainer is only free Thursday at nine. Flexible is for activities that can move to fit the people. The choice screen names the tradeoff outright: fixed happens "regardless of agent conflicts," flexible "can accommodate your agent."

A 'Scheduling next steps' popup for a flexible bulk activity, stating that Zoom will look for default activity time in agents' schedules and assign the activity at times that have the least impact to requirements, and that agents without default activity time will not be scheduled.
Flexible mode states the rule before the supervisor commits: the activity goes where it has least impact, and agents without default activity time won't be scheduled.

In fixed mode the system won't resolve conflicts on the supervisor's behalf, and it says so before anything is submitted. Then it lists them — every conflicting activity type, the number of agents behind it, and a decision per row: schedule anyway, or don't include them.

Two of those rows carry the weight. Out of office is an obvious conflict. Not scheduled isn't — an agent with no schedule that day reads to a naive system as available. Defining that absence as something the supervisor has to answer for is the difference between two hundred agents getting trained and ten of them being booked for a session on a day they aren't working.

A 'Resolve conflicts' screen for a fixed bulk activity: each conflicting activity type is a row showing the number of affected agents and a choice to schedule anyway or not include them, with 'Not scheduled' and 'Out of office' appearing as first-class conflict rows.
Every conflicting activity type, the agents behind it, and a decision per row. "Not scheduled" and "Out of office" are conflicts the supervisor has to answer for.

Flexible mode inverts the mechanism and keeps the standard. Because the activity can move, the system places it where it has least impact to requirements — then accounts for who it couldn't fit, with a reason for each: the available time couldn't accommodate the duration, the minimum group size couldn't be honored, they had no schedule or were out of office. Each reason opens to names.

The 'Unscheduled agent details' screen from flexible bulk scheduling: the count of agents that couldn't be scheduled is broken into categorized reasons — available default time can't accommodate the activity duration, the minimum requested agents can't be honored, and no schedule or out of office — each expandable to the agents it applies to.
Unscheduled agents are grouped by reason, and each group opens to the agents in it.

Under both modes runs a state machine the supervisor can read at any moment — draft, processing, ready, scheduled, failed — each state defining what it means and what can be done from it. Failed offers a retry. Scheduled offers the list of who didn't make it.

A specification diagram for the bulk-activity status states — Draft, Processing, Ready, Scheduled, and Failed — each with a plain-language definition and the actions available from it, such as 'Try again' from Failed and viewing the unscheduled agents from Scheduled.
Part of the spec I wrote for engineering, defining what each state means and what the supervisor can do from it.
Rotational scheduling

Automating shifts that agents cycle through

Some contact centers can't run on fixed hours. Agents rotate — mornings for two weeks, then evenings, then nights — so no one carries the worst shift permanently. Before this work, supervisors did that by hand, moving people between shifts week after week. At a few hundred agents, that's a task you eventually do wrong.

The design models the thing itself rather than the chore: a template holds an ordered set of shifts, agents are assigned to a shift, and each time schedules generate, everyone advances to the next one and wraps around at the end. The supervisor sets how many weeks an agent stays in a shift before moving on.

A discovery diagram showing how a rotation plays out over time: three shifts with groups of agents cycling to the next shift at each 'schedule generation occurs' step and wrapping around at the end.
How a rotation actually plays out. I drew this during discovery to work out the model — each generation advances every agent to the next shift and wraps at the end.

Order is a real configuration step, not a byproduct — the sequence agents move through is what determines whose hours change when, so it's editable directly. And because a rotation is a process already in motion, the supervisor can advance or revert everyone a position by hand, for the week that has to be re-run.

The part that took the most thought is the failure the system checks for. A rotation only produces even coverage if the shifts are balanced — and balance isn't a headcount. Four shifts can hold seven agents each and still be broken, if those sevens are drawn unevenly from the same scheduling group. Somebody's queue ends up short-staffed on rotation week three, and nobody finds out until it happens. So the template checks distribution rather than totals, and names the groups it found uneven.

A rotational template's imbalanced-agents warning with visibly uneven agent counts across four shifts, for example three, seven, ten, and seven. The same imbalanced-agents warning firing even though all four shifts show seven agents each, because the distribution within a scheduling group is uneven; the warning names the affected scheduling groups.
Both templates hold the same total agents; the one on the right has all four shifts at seven and still trips the warning, because the distribution within a scheduling group is uneven. The warning names the groups.

Smaller consequences get stated at the moment they matter. Duplicating a shift to save setup time doesn't bring its agents along — the modal says so before the supervisor clicks, because an empty shift that looks populated is a coverage gap waiting for a generation cycle to expose it.

A generated example week for a single shift, color-coded by activity — Voice, Break, and Meal — across the days, with a 'Refresh example' control.
A generated week for one shift — the output the supervisor is really configuring.
Intraday reforecasting

Adapting the schedule when the day changes

A contact center plans its day in advance and then the day happens. Volume runs hot, a queue backs up, and by ten in the morning the staffing the forecast asked for isn't the staffing the day needs. A supervisor can see that in the intraday report. What they couldn't do was act on it without rebuilding the afternoon by hand.

This is two features that are really one flow: recalculate what the rest of the day requires, then redistribute the breaks and lunches that were planned against the old numbers.

Reforecasting comes first, and it doesn't commit anything. The new staffing levels arrive as a preview, broken out by scheduling group, with a toggle that switches the table from absolute numbers to the difference — plus three, minus two, plus nine. The supervisor decides which groups take the change. Some queues need the new plan; some are fine. Applying to all of them because the system calculated all of them would be the easy version and the wrong one.

Reforecasted intraday staffing levels shown as a preview, broken out per scheduling group with checkboxes to select which groups to update, an 'Apply updates to schedule' action disabled until a group is selected, and a 'Show difference' toggle that switches the table to deltas.
Reforecasting produces a preview, not a change. Per scheduling group, with a toggle for the difference, and nothing applies until the supervisor picks which groups.

Once applied, the schedule marks what moved. The changed staffing values stay highlighted until the next time they change, so a recalculation that touched dozens of numbers doesn't arrive as a schedule that's simply different from the one the supervisor remembers.

The schedules page after applying reforecasted staffing levels, with the changed values highlighted so the recalculation stays legible after the fact.
Applied levels stay highlighted until they change again, so a recalculation across dozens of values remains legible after the fact.

Then the part that touches people directly. New staffing levels mean the breaks and lunches planned against the old ones are in the wrong places. Re-optimizing that is the system rearranging agents' afternoons — so it does it on a leash. The supervisor sets how far any activity may move and the window it may move within, separately for breaks and for meals. They choose which scheduling groups it may touch, defaulted to the ones that were just reforecasted. Breaks can be optimized without meals, or the reverse.

The 'Optimize breaks and meals' modal: scheduling-group scope pre-filled with the reforecasted groups, independent toggles for breaks and meals, and flex-increment and max-flex-time settings for each type, each with a tooltip defining what it governs.
The leash: how far breaks and meals may move, the window they may move within, and whose schedules are in scope — defaulted to the groups just reforecasted.

And it doesn't apply anything until the supervisor has seen it. The preview draws the result on the schedule itself: new positions filled in solid, previous positions left as outlines in the same color, so both are visible at once. Agents whose breaks didn't move are still shown — the view is the whole picture, not a filtered list of changes. The preview also states what happens on apply, including that agents will be notified.

A preview of optimized breaks and meals drawn on the real schedule: new positions filled in solid and previous positions shown as dotted outlines in the same activity color, with agents whose breaks didn't change still shown.
The preview draws new positions solid and previous positions as outlines, on the real schedule. Agents whose breaks didn't move are still shown.

Which they are, on their own schedule and by email. That notification is conditional: if a particular agent's day didn't actually change, they don't get told it did.

Sometimes the optimizer runs and concludes nothing should move. It says so — in the toast and in the email, in as many words. A recommendation of no change is a result, and the supervisor is told it explicitly rather than being left to infer it from a schedule that looks the same as before.

Beyond scheduling

Additional features across the product

Scheduling was where the standard got tested hardest, but the same thinking ran through the rest of the product.

The HR bridge. Two features linked WFM to the HR systems a company already ran on — one pulling time-off balances in from ADP, the other pushing paid hours out to payroll. They shared infrastructure: one place linking a WFM agent to their HR identity, sibling mapping tables under one settings home, and the same activity form behaving differently depending on whether the activity was time off or productive time. Most of my time went to what happens when a mapping breaks. Delete the activity and the mapping goes with it. Delete the HR code on the other side and the row stays, with its code field cleared and waiting for a new selection — because the activity still exists and still needs mapping. Either way the system says what it did. Clearing a mapping deliberately gets its own reassurance: the existing balance isn't deleted with it. These are people's vacation days and paychecks, so the system says when it touches them.

The HR out-of-office mapping page with a banner reading “Sick day” activity was deleted. The associated mapping was automatically removed. The same mapping page with a banner reading “SICK_LEAVE” HR code no longer exists. The associated mapping was automatically cleared — the Sick day row remains with its HR Code field reset to “Select code.”
Two ways a mapping breaks. Delete the activity and the row goes with it. Delete the HR code and the row stays, its code field cleared and waiting for a new selection.

Forecasting profiles. Admins were rebuilding the same forecast repeatedly because one forecast could only carry one set of targets. Profiles let them save targets per group and reuse them. Selecting a profile also restructures the wizard: the step that used to be "add scheduling groups" becomes "validate scheduling groups," a per-group check resolving to success, warning, or failure. A forecast built on thin historical data fails the check rather than producing a confident number nobody should trust.

The 'Validate scheduling groups' step of the forecast wizard: a per-group table showing SchedulingGroup1 Failed with reason 'Unable to sync data from server,' SchedulingGroup2 Warning with 'Insufficient historical data,' and SchedulingGroup3 through 5 Success, above a 'Generate forecast' button.
Selecting a profile turns the scheduling-group step into a validation gate. Groups that can't produce a reliable forecast fail the check before one runs.

Long-range capacity planning projects staffing need a year or more out, with growth and decay scenarios. The output is a hiring or reduction decision rather than a schedule, so the design is built around comparing scenarios instead of presenting one number.

Smaller features, same thinking. A frequently-used activities list that reorders itself by usage, with an explanation for why something dropped off and a manual override. A supervisor view showing whether an agent has synced their schedule to their own calendar — published isn't the same as received. Time-off approvals that propagate to every surface at once. A scheduling report that exports as a locked file with a timestamp, because a report that gets forwarded shouldn't be editable and should say when it was true.

Outcome

What carried across the work

Everything here shipped or was in build when I left at ten months. There are no post-launch metrics in this case study; I wasn't there to collect them.

Validation was direct: designs went in front of customers — sometimes with me, sometimes with the PM — and came back with feedback that shaped them.

What I'd carry forward isn't any of these features. It's the standard: a system that acts on people at scale owes them, and the person responsible for them, an account of what it did.