Every arrival on this page is a row in NYC Open Data — 311 Service Requests ↗

Staffing a contact centre from public data

New York's 311 line receives 17.4 million calls a year. This page takes its Spanish-language desk — 484,000 calls a year, 2.8% of the line and reported separately by the city, forecasts a week of them, works out how many people that needs, builds a roster that obeys Spanish working-time law, and puts a price on it.

01The data

Real, public, and not what it looks like.

Every contact below is a row in New York City's 311 Service Requests dataset — 8,736 hours of 52 whole weeks, fetched from the city's own API. Nothing here is generated.

These rows are service requests, not contacts. New York took 17.4M calls in Fiscal 2025 and closed 3.8M service requests. Only 6.1% of calls leave a phone-originated request behind, so sizing a phone floor straight from the open data would understaff it by about 16×. The hourly shape below is real; the voice level is scaled to the city's own published call count, and the web and app rows are self-service submissions that reach an agent only as deferred work, never as a queue.

The desk rostered here is the Spanish-language line, which New York reports separately: 484,000 calls a year, 2.8% of the whole operation. It is chosen because it is real, named, published and about twenty-five people — where the full 311 floor is over a thousand, which is a different kind of problem and not one a laptop should pretend to solve.

Rows3,167,94952 weeks of 2024
Calls (FY25)17.4Mcity's published figure
Request rate6.1%of calls leave a request
Answered in 30s80%NYC target and actual
Voice — calls per hourweek 40 of 52, starting 2024-01-01
Mon Tue Wed Thu Fri Sat Sun 00 Mon 00:00 — 40 calls 40 Tue 00:00 — 22 calls 22 Wed 00:00 — 39 calls 39 Thu 00:00 — 22 calls 22 Fri 00:00 — 48 calls 48 Sat 00:00 — 45 calls 45 Sun 00:00 — 63 calls 63 Mon 01:00 — 25 calls 25 Tue 01:00 — 22 calls 22 Wed 01:00 — 18 calls 18 Thu 01:00 — 20 calls 20 Fri 01:00 — 23 calls 23 Sat 01:00 — 42 calls 42 Sun 01:00 — 57 calls 57 Mon 02:00 — 14 calls 14 Tue 02:00 — 11 calls 11 Wed 02:00 — 16 calls 16 Thu 02:00 — 14 calls 14 Fri 02:00 — 20 calls 20 Sat 02:00 — 26 calls 26 Sun 02:00 — 42 calls 42 03 Mon 03:00 — 7 calls 7 Tue 03:00 — 9 calls 9 Wed 03:00 — 9 calls 9 Thu 03:00 — 9 calls 9 Fri 03:00 — 13 calls 13 Sat 03:00 — 13 calls 13 Sun 03:00 — 28 calls 28 Mon 04:00 — 16 calls 16 Tue 04:00 — 3 calls 3 Wed 04:00 — 10 calls 10 Thu 04:00 — 14 calls 14 Fri 04:00 — 14 calls 14 Sat 04:00 — 23 calls 23 Sun 04:00 — 24 calls 24 Mon 05:00 — 11 calls 11 Tue 05:00 — 10 calls 10 Wed 05:00 — 22 calls 22 Thu 05:00 — 10 calls 10 Fri 05:00 — 22 calls 22 Sat 05:00 — 15 calls 15 Sun 05:00 — 12 calls 12 06 Mon 06:00 — 21 calls 21 Tue 06:00 — 16 calls 16 Wed 06:00 — 25 calls 25 Thu 06:00 — 28 calls 28 Fri 06:00 — 34 calls 34 Sat 06:00 — 22 calls 22 Sun 06:00 — 16 calls 16 Mon 07:00 — 35 calls 35 Tue 07:00 — 48 calls 48 Wed 07:00 — 51 calls 51 Thu 07:00 — 43 calls 43 Fri 07:00 — 65 calls 65 Sat 07:00 — 23 calls 23 Sun 07:00 — 22 calls 22 Mon 08:00 — 57 calls 57 Tue 08:00 — 73 calls 73 Wed 08:00 — 91 calls 91 Thu 08:00 — 91 calls 91 Fri 08:00 — 92 calls 92 Sat 08:00 — 37 calls 37 Sun 08:00 — 34 calls 34 09 Mon 09:00 — 122 calls 122 Tue 09:00 — 108 calls 108 Wed 09:00 — 94 calls 94 Thu 09:00 — 91 calls 91 Fri 09:00 — 92 calls 92 Sat 09:00 — 51 calls 51 Sun 09:00 — 34 calls 34 Mon 10:00 — 153 calls 153 Tue 10:00 — 116 calls 116 Wed 10:00 — 104 calls 104 Thu 10:00 — 94 calls 94 Fri 10:00 — 118 calls 118 Sat 10:00 — 47 calls 47 Sun 10:00 — 36 calls 36 Mon 11:00 — 125 calls 125 Tue 11:00 — 131 calls 131 Wed 11:00 — 107 calls 107 Thu 11:00 — 111 calls 111 Fri 11:00 — 113 calls 113 Sat 11:00 — 46 calls 46 Sun 11:00 — 44 calls 44 12 Mon 12:00 — 98 calls 98 Tue 12:00 — 93 calls 93 Wed 12:00 — 100 calls 100 Thu 12:00 — 116 calls 116 Fri 12:00 — 115 calls 115 Sat 12:00 — 39 calls 39 Sun 12:00 — 43 calls 43 Mon 13:00 — 96 calls 96 Tue 13:00 — 99 calls 99 Wed 13:00 — 79 calls 79 Thu 13:00 — 117 calls 117 Fri 13:00 — 111 calls 111 Sat 13:00 — 54 calls 54 Sun 13:00 — 54 calls 54 Mon 14:00 — 122 calls 122 Tue 14:00 — 89 calls 89 Wed 14:00 — 106 calls 106 Thu 14:00 — 90 calls 90 Fri 14:00 — 100 calls 100 Sat 14:00 — 47 calls 47 Sun 14:00 — 32 calls 32 15 Mon 15:00 — 100 calls 100 Tue 15:00 — 87 calls 87 Wed 15:00 — 79 calls 79 Thu 15:00 — 108 calls 108 Fri 15:00 — 74 calls 74 Sat 15:00 — 43 calls 43 Sun 15:00 — 38 calls 38 Mon 16:00 — 84 calls 84 Tue 16:00 — 98 calls 98 Wed 16:00 — 59 calls 59 Thu 16:00 — 78 calls 78 Fri 16:00 — 76 calls 76 Sat 16:00 — 44 calls 44 Sun 16:00 — 26 calls 26 Mon 17:00 — 80 calls 80 Tue 17:00 — 67 calls 67 Wed 17:00 — 61 calls 61 Thu 17:00 — 69 calls 69 Fri 17:00 — 72 calls 72 Sat 17:00 — 36 calls 36 Sun 17:00 — 37 calls 37 18 Mon 18:00 — 57 calls 57 Tue 18:00 — 51 calls 51 Wed 18:00 — 46 calls 46 Thu 18:00 — 58 calls 58 Fri 18:00 — 37 calls 37 Sat 18:00 — 34 calls 34 Sun 18:00 — 39 calls 39 Mon 19:00 — 59 calls 59 Tue 19:00 — 42 calls 42 Wed 19:00 — 39 calls 39 Thu 19:00 — 45 calls 45 Fri 19:00 — 43 calls 43 Sat 19:00 — 36 calls 36 Sun 19:00 — 32 calls 32 Mon 20:00 — 46 calls 46 Tue 20:00 — 61 calls 61 Wed 20:00 — 33 calls 33 Thu 20:00 — 38 calls 38 Fri 20:00 — 28 calls 28 Sat 20:00 — 33 calls 33 Sun 20:00 — 28 calls 28 21 Mon 21:00 — 32 calls 32 Tue 21:00 — 38 calls 38 Wed 21:00 — 26 calls 26 Thu 21:00 — 48 calls 48 Fri 21:00 — 34 calls 34 Sat 21:00 — 24 calls 24 Sun 21:00 — 20 calls 20 Mon 22:00 — 30 calls 30 Tue 22:00 — 37 calls 37 Wed 22:00 — 21 calls 21 Thu 22:00 — 37 calls 37 Fri 22:00 — 22 calls 22 Sat 22:00 — 41 calls 41 Sun 22:00 — 21 calls 21 Mon 23:00 — 40 calls 40 Tue 23:00 — 42 calls 42 Wed 23:00 — 35 calls 35 Thu 23:00 — 48 calls 48 Fri 23:00 — 52 calls 52 Sat 23:00 — 54 calls 54 Sun 23:00 — 54 calls 54
Web and app — requests per hourweek 40 of 52, starting 2024-01-01
Mon Tue Wed Thu Fri Sat Sun 00 Mon 00:00 — 6 requests 6 Tue 00:00 — 5 requests 5 Wed 00:00 — 6 requests 6 Thu 00:00 — 4 requests 4 Fri 00:00 — 4 requests 4 Sat 00:00 — 9 requests 9 Sun 00:00 — 13 requests 13 Mon 01:00 — 3 requests 3 Tue 01:00 — 3 requests 3 Wed 01:00 — 4 requests 4 Thu 01:00 — 2 requests 2 Fri 01:00 — 4 requests 4 Sat 01:00 — 5 requests 5 Sun 01:00 — 8 requests 8 Mon 02:00 — 3 requests 3 Tue 02:00 — 2 requests 2 Wed 02:00 — 2 requests 2 Thu 02:00 — 1 requests 1 Fri 02:00 — 1 requests 1 Sat 02:00 — 3 requests 3 Sun 02:00 — 5 requests 5 03 Mon 03:00 — 2 requests 2 Tue 03:00 — 2 requests 2 Wed 03:00 — 2 requests 2 Thu 03:00 — 1 requests 1 Fri 03:00 — 1 requests 1 Sat 03:00 — 3 requests 3 Sun 03:00 — 3 requests 3 Mon 04:00 — 2 requests 2 Tue 04:00 — 1 requests 1 Wed 04:00 — 1 requests 1 Thu 04:00 — 2 requests 2 Fri 04:00 — 2 requests 2 Sat 04:00 — 3 requests 3 Sun 04:00 — 3 requests 3 Mon 05:00 — 2 requests 2 Tue 05:00 — 2 requests 2 Wed 05:00 — 2 requests 2 Thu 05:00 — 2 requests 2 Fri 05:00 — 2 requests 2 Sat 05:00 — 2 requests 2 Sun 05:00 — 2 requests 2 06 Mon 06:00 — 4 requests 4 Tue 06:00 — 5 requests 5 Wed 06:00 — 4 requests 4 Thu 06:00 — 5 requests 5 Fri 06:00 — 5 requests 5 Sat 06:00 — 2 requests 2 Sun 06:00 — 2 requests 2 Mon 07:00 — 8 requests 8 Tue 07:00 — 8 requests 8 Wed 07:00 — 7 requests 7 Thu 07:00 — 9 requests 9 Fri 07:00 — 7 requests 7 Sat 07:00 — 4 requests 4 Sun 07:00 — 4 requests 4 Mon 08:00 — 9 requests 9 Tue 08:00 — 10 requests 10 Wed 08:00 — 10 requests 10 Thu 08:00 — 8 requests 8 Fri 08:00 — 10 requests 10 Sat 08:00 — 5 requests 5 Sun 08:00 — 5 requests 5 09 Mon 09:00 — 9 requests 9 Tue 09:00 — 10 requests 10 Wed 09:00 — 8 requests 8 Thu 09:00 — 9 requests 9 Fri 09:00 — 10 requests 10 Sat 09:00 — 7 requests 7 Sun 09:00 — 7 requests 7 Mon 10:00 — 10 requests 10 Tue 10:00 — 8 requests 8 Wed 10:00 — 9 requests 9 Thu 10:00 — 8 requests 8 Fri 10:00 — 9 requests 9 Sat 10:00 — 7 requests 7 Sun 10:00 — 8 requests 8 Mon 11:00 — 10 requests 10 Tue 11:00 — 9 requests 9 Wed 11:00 — 9 requests 9 Thu 11:00 — 10 requests 10 Fri 11:00 — 9 requests 9 Sat 11:00 — 7 requests 7 Sun 11:00 — 10 requests 10 12 Mon 12:00 — 9 requests 9 Tue 12:00 — 8 requests 8 Wed 12:00 — 8 requests 8 Thu 12:00 — 9 requests 9 Fri 12:00 — 8 requests 8 Sat 12:00 — 6 requests 6 Sun 12:00 — 8 requests 8 Mon 13:00 — 8 requests 8 Tue 13:00 — 7 requests 7 Wed 13:00 — 8 requests 8 Thu 13:00 — 7 requests 7 Fri 13:00 — 8 requests 8 Sat 13:00 — 8 requests 8 Sun 13:00 — 7 requests 7 Mon 14:00 — 7 requests 7 Tue 14:00 — 8 requests 8 Wed 14:00 — 6 requests 6 Thu 14:00 — 7 requests 7 Fri 14:00 — 8 requests 8 Sat 14:00 — 8 requests 8 Sun 14:00 — 7 requests 7 15 Mon 15:00 — 9 requests 9 Tue 15:00 — 7 requests 7 Wed 15:00 — 7 requests 7 Thu 15:00 — 8 requests 8 Fri 15:00 — 8 requests 8 Sat 15:00 — 8 requests 8 Sun 15:00 — 8 requests 8 Mon 16:00 — 9 requests 9 Tue 16:00 — 7 requests 7 Wed 16:00 — 8 requests 8 Thu 16:00 — 8 requests 8 Fri 16:00 — 7 requests 7 Sat 16:00 — 9 requests 9 Sun 16:00 — 9 requests 9 Mon 17:00 — 7 requests 7 Tue 17:00 — 8 requests 8 Wed 17:00 — 8 requests 8 Thu 17:00 — 7 requests 7 Fri 17:00 — 7 requests 7 Sat 17:00 — 8 requests 8 Sun 17:00 — 10 requests 10 18 Mon 18:00 — 8 requests 8 Tue 18:00 — 8 requests 8 Wed 18:00 — 8 requests 8 Thu 18:00 — 8 requests 8 Fri 18:00 — 9 requests 9 Sat 18:00 — 8 requests 8 Sun 18:00 — 11 requests 11 Mon 19:00 — 8 requests 8 Tue 19:00 — 6 requests 6 Wed 19:00 — 6 requests 6 Thu 19:00 — 8 requests 8 Fri 19:00 — 8 requests 8 Sat 19:00 — 7 requests 7 Sun 19:00 — 10 requests 10 Mon 20:00 — 8 requests 8 Tue 20:00 — 8 requests 8 Wed 20:00 — 7 requests 7 Thu 20:00 — 7 requests 7 Fri 20:00 — 9 requests 9 Sat 20:00 — 9 requests 9 Sun 20:00 — 11 requests 11 21 Mon 21:00 — 10 requests 10 Tue 21:00 — 9 requests 9 Wed 21:00 — 10 requests 10 Thu 21:00 — 10 requests 10 Fri 21:00 — 10 requests 10 Sat 21:00 — 12 requests 12 Sun 21:00 — 14 requests 14 Mon 22:00 — 9 requests 9 Tue 22:00 — 11 requests 11 Wed 22:00 — 9 requests 9 Thu 22:00 — 9 requests 9 Fri 22:00 — 14 requests 14 Sat 22:00 — 16 requests 16 Sun 22:00 — 15 requests 15 Mon 23:00 — 6 requests 6 Tue 23:00 — 7 requests 7 Wed 23:00 — 7 requests 7 Thu 23:00 — 7 requests 7 Fri 23:00 — 11 requests 11 Sat 23:00 — 12 requests 12 Sun 23:00 — 14 requests 14

The two shapes are genuinely different, which is the reason to keep them apart. Voice peaks hard on Mon mid-morning and again after lunch; self-service spreads flatter and runs later, because nobody has to wait for an agent to be awake.

02The forecast

A ridge regression on a seasonal basis, scored the way the operation feels it rather than the way a paper would.

Calls per hour, forecast against actualweek 40 of 52, starting 2024-01-01
165 124 82 41 0 Mon Tue Wed Thu Fri Sat Sun
what actually arrived forecast, unbiased forecast staffed to, +20%calls/hour
Trained on6,384hourly observations, 38 weeks
Features32columns in the design matrix
R² in log space0.834residual sd 0.273
Hourly error15.6%8.0 calls/hour on a mean of 51
sMAPE30.7%symmetric, over hours with traffic
Week total+28.7%11,124 staffed vs 8,642 arrived
Uplift+20%chosen from the history
Three percentages because they answer three questions. 15.6% is how far the unbiased forecast sits from a typical hour — that is the model's accuracy, and the number to compare against anyone else's. 30.7% is the same thing scored symmetrically over hours that actually had traffic, so a quiet 3am hour cannot flatter or wreck it. +28.7% is the week's total once the uplift is added, and it is deliberately positive: that is the cushion being bought, not an error. Read the first as quality and the third as policy.

The model, written out

It is one equation. h is the hour of the day, w the hour of the week, t the hour index since the history starts:

log1p(callst)  =  β0
  + Σk=1..4 [ ak sin(2πk·h/24) + bk cos(2πk·h/24) ]   — the shape of a day, four harmonics
  + weekendt · Σk=1..4 [ ck sin(2πk·h/24) + dk cos(2πk·h/24) ]   — Saturday is a different shape, not a smaller one
  + Σk=1..3 [ ek sin(2πk·w/168) + fk cos(2πk·w/168) ]   — the slide from Monday to Friday
  + Σd=Tue..Sun gd·1[dowt=d]   — six day levels, against Monday
  + τ·(t/168)   — trend, read as growth per week
  + λ1 log1p(callst−168) + λ2 log1p(callst−336)   — same hour one and two weeks ago

callst  =  (exp(fit) − 1)  ×  1.0350  smearing  ×  1.20  uplift

32 coefficients, fitted in closed form by ridge — one np.linalg.solve, no iteration, no gradient descent, no random seed. Given the same history it returns the same numbers every time.

Is it "pure ML"?

No, and the distinction is worth being honest about. Nothing here discovers that call volume has a daily rhythm: the sines and cosines are written into the design matrix by hand, and so are the weekend interaction, the day dummies and the two lags. What is learned is 32 numbers — the weights on features somebody already decided were the right ones.

A gradient-boosted tree or a neural net would be handed raw timestamps and expected to find the structure itself. On 38 weeks of one queue that trade is a bad one: there are 6,384 rows, the structure is genuinely sinusoidal, and a model that finds seasonality on its own needs far more data to match a model that was told. The payoff for the small model is that every coefficient is inspectable — the table below is not a feature-importance approximation, it is the actual parameters — and that it cannot invent a pattern that was never encoded.

What it does not know

There are no external regressors. No public-holiday flag, no weather, no marketing calendar, no outage feed. The model sees its own past and the clock, nothing else. That is a real limit and it shows up in a specific place: a day that is anomalous for a reason outside the data is absorbed into the lag terms and then echoes for two weeks, because callst−168 and callst−336 are inputs. A holiday is forecast as if it were an ordinary Tuesday, and the two Tuesdays after it inherit the dent.

Adding a holiday dummy is the cheapest real improvement available here, and it is not done: the calendar is jurisdiction-specific and this report is built from one city's data, so a flag fitted on it would not transfer. Worth stating plainly rather than leaving a reader to assume the model handles days it has never been told about.

Why it is this small

The fit happens on log1p rather than on calls, which buys two things: the seasonality becomes multiplicative — a Monday peak is a ratio above the week's level, not a fixed number of calls — and a prediction can never come out negative, which an additive fit on a queue that idles near zero will happily do.

It is deliberately small. 6,384 observations against 32 parameters is about 199 rows per column, and anything heavier would be fitting the noise in the shoulders of the morning peak. The penalty leaves the intercept alone so the level stays free, which is also why the residuals come out centred to 6e-15 — and that in turn is why the smearing factor is a clean 1.0350 rather than something that has absorbed a bias.

What the model leans oncoefficient × the spread of its own column, which is the only way to compare a dummy with a harmonic
feature standardised effect
day cos×1 -0.408
day sin×1 -0.205
weekend day cos×1 +0.180
day sin×2 -0.160
is Sat -0.145
is Sun -0.122
day cos×3 +0.119
day cos×2 +0.118
same hour last week +0.097
is Fri -0.076
The first daily harmonic alone carries more weight than every day-of-week level put together. That is the two-humped day — morning rush, lunch dip, evening rush — and it is why a model with no seasonal basis at all has to learn the shape from the lag features and never quite does.
Error week by week, not just on averagemean absolute error in calls per hour, 44 weeks scored, refitting before each one
24 18 12 6 0
seasonal naive 4-week mean ridge seasonalcalls/hour
A pooled MAE hides whether a model is steadily better or merely better on average. Here the ridge line sits under the naive one in almost every week rather than winning a few by a lot — which is the version worth having, because a forecast that is reliably slightly better is schedulable and one that is wildly better in some weeks is not.
Would more history help?test error against weeks of training data, each model refit on only that much history and every one scored on the same final weeks
14.34 13.93 13.52 13.11 12.70 6 weeks of history: 13.562 calls/hour 13.56 6w 10 weeks of history: 13.916 calls/hour 13.92 10w 16 weeks of history: 14.062 calls/hour 14.06 16w 24 weeks of history: 13.533 calls/hour 13.53 24w 32 weeks of history: 13.121 calls/hour 13.12 32w 40 weeks of history: 13.350 calls/hour 13.35 40w axis spans 12.70–14.34 calls/hour, not zero — best is 6.7% under the worst
It stops helping. Error bottoms out around 32 weeks of history and flattens after that: the weekly shape is learned quickly and the extra months mostly add drift the trend term already handles. Worth knowing before anyone is asked to warehouse three years of interval data.

The uplift is deliberate. An unbiased forecast is wrong in the expensive direction half the time, because a missing agent costs a queue and a spare one costs an hour of salary. How far above the mean to staff is settled by backtesting rather than by taste:

What each point of service level costsRecovered is measured against perfect foresight, not against 100%.
uplift rostered hours service recovered hours short
+0% 77,424 84.6% 5,797
+5% 80,166 89.8% 4,539
+10% 82,926 94.2% 3,489
+15% 85,480 97.9% 2,675
+20% 88,065 100.0% 2,119
+30% 93,416 100.0% 1,310

03How many people that needs

Three channels, three models — because they are three different problems.

Voice goes through Erlang C at 290s handle time against 80% in 30s. The queue is real, the caller is waiting, and the formula answers the only question that matters: how many people keep the delay short.

Service requests do not work that way at all: nobody is on the line, so the question is not how long a queue gets but whether enough agent-hours exist inside the 24-hour cycle time to clear the work. That is conservation, not queueing, and it is measured as COPC's On Time rather than as a service level in seconds. Running email through Erlang C is the most common mistake in this field and it errs in both directions.

Agents needed — voiceErlang C, with shrinkage
Mon Tue Wed Thu Fri Sat Sun 00 Mon 00:00 — 10 10 Tue 00:00 — 9 9 Wed 00:00 — 9 9 Thu 00:00 — 9 9 Fri 00:00 — 9 9 Sat 00:00 — 12 12 Sun 00:00 — 13 13 Mon 01:00 — 8 8 Tue 01:00 — 8 8 Wed 01:00 — 8 8 Thu 01:00 — 8 8 Fri 01:00 — 8 8 Sat 01:00 — 10 10 Sun 01:00 — 12 12 Mon 02:00 — 6 6 Tue 02:00 — 6 6 Wed 02:00 — 6 6 Thu 02:00 — 6 6 Fri 02:00 — 6 6 Sat 02:00 — 9 9 Sun 02:00 — 9 9 03 Mon 03:00 — 5 5 Tue 03:00 — 5 5 Wed 03:00 — 5 5 Thu 03:00 — 5 5 Fri 03:00 — 5 5 Sat 03:00 — 8 8 Sun 03:00 — 8 8 Mon 04:00 — 5 5 Tue 04:00 — 5 5 Wed 04:00 — 5 5 Thu 04:00 — 5 5 Fri 04:00 — 5 5 Sat 04:00 — 6 6 Sun 04:00 — 6 6 Mon 05:00 — 6 6 Tue 05:00 — 6 6 Wed 05:00 — 6 6 Thu 05:00 — 5 5 Fri 05:00 — 5 5 Sat 05:00 — 6 6 Sun 05:00 — 6 6 06 Mon 06:00 — 8 8 Tue 06:00 — 8 8 Wed 06:00 — 8 8 Thu 06:00 — 8 8 Fri 06:00 — 8 8 Sat 06:00 — 6 6 Sun 06:00 — 6 6 Mon 07:00 — 12 12 Tue 07:00 — 12 12 Wed 07:00 — 10 10 Thu 07:00 — 10 10 Fri 07:00 — 10 10 Sat 07:00 — 8 8 Sun 07:00 — 8 8 Mon 08:00 — 16 16 Tue 08:00 — 16 16 Wed 08:00 — 16 16 Thu 08:00 — 16 16 Fri 08:00 — 15 15 Sat 08:00 — 9 9 Sun 08:00 — 9 9 09 Mon 09:00 — 22 22 Tue 09:00 — 20 20 Wed 09:00 — 19 19 Thu 09:00 — 19 19 Fri 09:00 — 19 19 Sat 09:00 — 10 10 Sun 09:00 — 10 10 Mon 10:00 — 23 23 Tue 10:00 — 22 22 Wed 10:00 — 22 22 Thu 10:00 — 20 20 Fri 10:00 — 20 20 Sat 10:00 — 12 12 Sun 10:00 — 12 12 Mon 11:00 — 23 23 Tue 11:00 — 22 22 Wed 11:00 — 22 22 Thu 11:00 — 22 22 Fri 11:00 — 20 20 Sat 11:00 — 12 12 Sun 11:00 — 12 12 12 Mon 12:00 — 22 22 Tue 12:00 — 22 22 Wed 12:00 — 22 22 Thu 12:00 — 20 20 Fri 12:00 — 20 20 Sat 12:00 — 12 12 Sun 12:00 — 12 12 Mon 13:00 — 22 22 Tue 13:00 — 20 20 Wed 13:00 — 20 20 Thu 13:00 — 20 20 Fri 13:00 — 19 19 Sat 13:00 — 12 12 Sun 13:00 — 12 12 Mon 14:00 — 20 20 Tue 14:00 — 19 19 Wed 14:00 — 20 20 Thu 14:00 — 19 19 Fri 14:00 — 19 19 Sat 14:00 — 12 12 Sun 14:00 — 12 12 15 Mon 15:00 — 20 20 Tue 15:00 — 19 19 Wed 15:00 — 19 19 Thu 15:00 — 18 18 Fri 15:00 — 18 18 Sat 15:00 — 12 12 Sun 15:00 — 12 12 Mon 16:00 — 18 18 Tue 16:00 — 18 18 Wed 16:00 — 18 18 Thu 16:00 — 16 16 Fri 16:00 — 16 16 Sat 16:00 — 10 10 Sun 16:00 — 10 10 Mon 17:00 — 16 16 Tue 17:00 — 16 16 Wed 17:00 — 16 16 Thu 17:00 — 15 15 Fri 17:00 — 15 15 Sat 17:00 — 10 10 Sun 17:00 — 10 10 18 Mon 18:00 — 13 13 Tue 18:00 — 13 13 Wed 18:00 — 13 13 Thu 18:00 — 13 13 Fri 18:00 — 12 12 Sat 18:00 — 10 10 Sun 18:00 — 10 10 Mon 19:00 — 12 12 Tue 19:00 — 12 12 Wed 19:00 — 12 12 Thu 19:00 — 12 12 Fri 19:00 — 10 10 Sat 19:00 — 10 10 Sun 19:00 — 9 9 Mon 20:00 — 10 10 Tue 20:00 — 10 10 Wed 20:00 — 10 10 Thu 20:00 — 10 10 Fri 20:00 — 10 10 Sat 20:00 — 9 9 Sun 20:00 — 9 9 21 Mon 21:00 — 10 10 Tue 21:00 — 10 10 Wed 21:00 — 10 10 Thu 21:00 — 10 10 Fri 21:00 — 9 9 Sat 21:00 — 9 9 Sun 21:00 — 9 9 Mon 22:00 — 10 10 Tue 22:00 — 10 10 Wed 22:00 — 10 10 Thu 22:00 — 9 9 Fri 22:00 — 9 9 Sat 22:00 — 10 10 Sun 22:00 — 9 9 Mon 23:00 — 10 10 Tue 23:00 — 10 10 Wed 23:00 — 10 10 Thu 23:00 — 10 10 Fri 23:00 — 10 10 Sat 23:00 — 12 12 Sun 23:00 — 10 10
Agents needed — service requestsbacklog inside the cycle time
Mon Tue Wed Thu Fri Sat Sun 00 Mon 00:00 — 1 1 Tue 00:00 — 1 1 Wed 00:00 — 1 1 Thu 00:00 — 1 1 Fri 00:00 — 1 1 Sat 00:00 — 2 2 Sun 00:00 — 2 2 Mon 01:00 — 1 1 Tue 01:00 — 1 1 Wed 01:00 — 1 1 Thu 01:00 — 1 1 Fri 01:00 — 1 1 Sat 01:00 — 2 2 Sun 01:00 — 2 2 Mon 02:00 — 1 1 Tue 02:00 — 1 1 Wed 02:00 — 1 1 Thu 02:00 — 1 1 Fri 02:00 — 1 1 Sat 02:00 — 2 2 Sun 02:00 — 2 2 03 Mon 03:00 — 1 1 Tue 03:00 — 1 1 Wed 03:00 — 1 1 Thu 03:00 — 1 1 Fri 03:00 — 1 1 Sat 03:00 — 1 1 Sun 03:00 — 2 2 Mon 04:00 — 1 1 Tue 04:00 — 1 1 Wed 04:00 — 1 1 Thu 04:00 — 1 1 Fri 04:00 — 1 1 Sat 04:00 — 1 1 Sun 04:00 — 2 2 Mon 05:00 — 1 1 Tue 05:00 — 1 1 Wed 05:00 — 1 1 Thu 05:00 — 1 1 Fri 05:00 — 1 1 Sat 05:00 — 1 1 Sun 05:00 — 1 1 06 Mon 06:00 — 1 1 Tue 06:00 — 1 1 Wed 06:00 — 1 1 Thu 06:00 — 1 1 Fri 06:00 — 1 1 Sat 06:00 — 1 1 Sun 06:00 — 1 1 Mon 07:00 — 1 1 Tue 07:00 — 1 1 Wed 07:00 — 1 1 Thu 07:00 — 1 1 Fri 07:00 — 1 1 Sat 07:00 — 1 1 Sun 07:00 — 1 1 Mon 08:00 — 1 1 Tue 08:00 — 1 1 Wed 08:00 — 1 1 Thu 08:00 — 1 1 Fri 08:00 — 1 1 Sat 08:00 — 1 1 Sun 08:00 — 1 1 09 Mon 09:00 — 1 1 Tue 09:00 — 1 1 Wed 09:00 — 1 1 Thu 09:00 — 1 1 Fri 09:00 — 1 1 Sat 09:00 — 1 1 Sun 09:00 — 1 1 Mon 10:00 — 1 1 Tue 10:00 — 1 1 Wed 10:00 — 2 2 Thu 10:00 — 1 1 Fri 10:00 — 2 2 Sat 10:00 — 1 1 Sun 10:00 — 2 2 Mon 11:00 — 1 1 Tue 11:00 — 1 1 Wed 11:00 — 2 2 Thu 11:00 — 2 2 Fri 11:00 — 2 2 Sat 11:00 — 1 1 Sun 11:00 — 2 2 12 Mon 12:00 — 1 1 Tue 12:00 — 1 1 Wed 12:00 — 2 2 Thu 12:00 — 2 2 Fri 12:00 — 2 2 Sat 12:00 — 1 1 Sun 12:00 — 2 2 Mon 13:00 — 1 1 Tue 13:00 — 1 1 Wed 13:00 — 2 2 Thu 13:00 — 2 2 Fri 13:00 — 2 2 Sat 13:00 — 2 2 Sun 13:00 — 1 1 Mon 14:00 — 1 1 Tue 14:00 — 1 1 Wed 14:00 — 1 1 Thu 14:00 — 2 2 Fri 14:00 — 2 2 Sat 14:00 — 2 2 Sun 14:00 — 2 2 15 Mon 15:00 — 1 1 Tue 15:00 — 1 1 Wed 15:00 — 2 2 Thu 15:00 — 2 2 Fri 15:00 — 1 1 Sat 15:00 — 2 2 Sun 15:00 — 2 2 Mon 16:00 — 1 1 Tue 16:00 — 1 1 Wed 16:00 — 2 2 Thu 16:00 — 1 1 Fri 16:00 — 2 2 Sat 16:00 — 2 2 Sun 16:00 — 2 2 Mon 17:00 — 1 1 Tue 17:00 — 1 1 Wed 17:00 — 1 1 Thu 17:00 — 2 2 Fri 17:00 — 1 1 Sat 17:00 — 1 1 Sun 17:00 — 2 2 18 Mon 18:00 — 1 1 Tue 18:00 — 1 1 Wed 18:00 — 2 2 Thu 18:00 — 2 2 Fri 18:00 — 2 2 Sat 18:00 — 2 2 Sun 18:00 — 2 2 Mon 19:00 — 1 1 Tue 19:00 — 1 1 Wed 19:00 — 1 1 Thu 19:00 — 1 1 Fri 19:00 — 2 2 Sat 19:00 — 1 1 Sun 19:00 — 2 2 Mon 20:00 — 1 1 Tue 20:00 — 2 2 Wed 20:00 — 2 2 Thu 20:00 — 2 2 Fri 20:00 — 2 2 Sat 20:00 — 2 2 Sun 20:00 — 2 2 21 Mon 21:00 — 1 1 Tue 21:00 — 2 2 Wed 21:00 — 2 2 Thu 21:00 — 2 2 Fri 21:00 — 2 2 Sat 21:00 — 2 2 Sun 21:00 — 2 2 Mon 22:00 — 1 1 Tue 22:00 — 2 2 Wed 22:00 — 2 2 Thu 22:00 — 2 2 Fri 22:00 — 2 2 Sat 22:00 — 2 2 Sun 22:00 — 2 2 Mon 23:00 — 1 1 Tue 23:00 — 2 2 Wed 23:00 — 2 2 Thu 23:00 — 2 2 Fri 23:00 — 2 2 Sat 23:00 — 2 2 Sun 23:00 — 2 2
Peak hour24agents, Mon 10:00
Offered load12erlangs at the peak
√-staffing check16N = R + √R
Erlang A abandon0.0%at that staffing
The square-root check is not decoration. N = R + β√R tracks the exact models across four orders of magnitude, and it catches the class of error a black-box Erlang routine absorbs silently — an interval length confused, seconds typed where minutes were meant, a daily forecast fed to an hourly model.
Agents needed — both channelsweek 40 of 52, starting 2024-01-01
Mon Tue Wed Thu Fri Sat Sun 00 Mon 00:00 — 11 11 Tue 00:00 — 10 10 Wed 00:00 — 10 10 Thu 00:00 — 10 10 Fri 00:00 — 10 10 Sat 00:00 — 14 14 Sun 00:00 — 15 15 Mon 01:00 — 9 9 Tue 01:00 — 9 9 Wed 01:00 — 9 9 Thu 01:00 — 9 9 Fri 01:00 — 9 9 Sat 01:00 — 12 12 Sun 01:00 — 14 14 Mon 02:00 — 7 7 Tue 02:00 — 7 7 Wed 02:00 — 7 7 Thu 02:00 — 7 7 Fri 02:00 — 7 7 Sat 02:00 — 11 11 Sun 02:00 — 11 11 03 Mon 03:00 — 6 6 Tue 03:00 — 6 6 Wed 03:00 — 6 6 Thu 03:00 — 6 6 Fri 03:00 — 6 6 Sat 03:00 — 9 9 Sun 03:00 — 10 10 Mon 04:00 — 6 6 Tue 04:00 — 6 6 Wed 04:00 — 6 6 Thu 04:00 — 6 6 Fri 04:00 — 6 6 Sat 04:00 — 7 7 Sun 04:00 — 8 8 Mon 05:00 — 7 7 Tue 05:00 — 7 7 Wed 05:00 — 7 7 Thu 05:00 — 6 6 Fri 05:00 — 6 6 Sat 05:00 — 7 7 Sun 05:00 — 7 7 06 Mon 06:00 — 9 9 Tue 06:00 — 9 9 Wed 06:00 — 9 9 Thu 06:00 — 9 9 Fri 06:00 — 9 9 Sat 06:00 — 7 7 Sun 06:00 — 7 7 Mon 07:00 — 13 13 Tue 07:00 — 13 13 Wed 07:00 — 11 11 Thu 07:00 — 11 11 Fri 07:00 — 11 11 Sat 07:00 — 9 9 Sun 07:00 — 9 9 Mon 08:00 — 17 17 Tue 08:00 — 17 17 Wed 08:00 — 17 17 Thu 08:00 — 17 17 Fri 08:00 — 16 16 Sat 08:00 — 10 10 Sun 08:00 — 10 10 09 Mon 09:00 — 23 23 Tue 09:00 — 21 21 Wed 09:00 — 20 20 Thu 09:00 — 20 20 Fri 09:00 — 20 20 Sat 09:00 — 11 11 Sun 09:00 — 11 11 Mon 10:00 — 24 24 Tue 10:00 — 23 23 Wed 10:00 — 24 24 Thu 10:00 — 21 21 Fri 10:00 — 22 22 Sat 10:00 — 13 13 Sun 10:00 — 14 14 Mon 11:00 — 24 24 Tue 11:00 — 23 23 Wed 11:00 — 24 24 Thu 11:00 — 24 24 Fri 11:00 — 22 22 Sat 11:00 — 13 13 Sun 11:00 — 14 14 12 Mon 12:00 — 23 23 Tue 12:00 — 23 23 Wed 12:00 — 24 24 Thu 12:00 — 22 22 Fri 12:00 — 22 22 Sat 12:00 — 13 13 Sun 12:00 — 14 14 Mon 13:00 — 23 23 Tue 13:00 — 21 21 Wed 13:00 — 22 22 Thu 13:00 — 22 22 Fri 13:00 — 21 21 Sat 13:00 — 14 14 Sun 13:00 — 13 13 Mon 14:00 — 21 21 Tue 14:00 — 20 20 Wed 14:00 — 21 21 Thu 14:00 — 21 21 Fri 14:00 — 21 21 Sat 14:00 — 14 14 Sun 14:00 — 14 14 15 Mon 15:00 — 21 21 Tue 15:00 — 20 20 Wed 15:00 — 21 21 Thu 15:00 — 20 20 Fri 15:00 — 19 19 Sat 15:00 — 14 14 Sun 15:00 — 14 14 Mon 16:00 — 19 19 Tue 16:00 — 19 19 Wed 16:00 — 20 20 Thu 16:00 — 17 17 Fri 16:00 — 18 18 Sat 16:00 — 12 12 Sun 16:00 — 12 12 Mon 17:00 — 17 17 Tue 17:00 — 17 17 Wed 17:00 — 17 17 Thu 17:00 — 17 17 Fri 17:00 — 16 16 Sat 17:00 — 11 11 Sun 17:00 — 12 12 18 Mon 18:00 — 14 14 Tue 18:00 — 14 14 Wed 18:00 — 15 15 Thu 18:00 — 15 15 Fri 18:00 — 14 14 Sat 18:00 — 12 12 Sun 18:00 — 12 12 Mon 19:00 — 13 13 Tue 19:00 — 13 13 Wed 19:00 — 13 13 Thu 19:00 — 13 13 Fri 19:00 — 12 12 Sat 19:00 — 11 11 Sun 19:00 — 11 11 Mon 20:00 — 11 11 Tue 20:00 — 12 12 Wed 20:00 — 12 12 Thu 20:00 — 12 12 Fri 20:00 — 12 12 Sat 20:00 — 11 11 Sun 20:00 — 11 11 21 Mon 21:00 — 11 11 Tue 21:00 — 12 12 Wed 21:00 — 12 12 Thu 21:00 — 12 12 Fri 21:00 — 11 11 Sat 21:00 — 11 11 Sun 21:00 — 11 11 Mon 22:00 — 11 11 Tue 22:00 — 12 12 Wed 22:00 — 12 12 Thu 22:00 — 11 11 Fri 22:00 — 11 11 Sat 22:00 — 12 12 Sun 22:00 — 11 11 Mon 23:00 — 11 11 Tue 23:00 — 12 12 Wed 23:00 — 12 12 Thu 23:00 — 12 12 Fri 23:00 — 12 12 Sat 23:00 — 14 14 Sun 23:00 — 12 12

04The rules the roster has to obey

Every constraint, what it means, and whether it is actually the law.

A roster is only interesting if it is legal, and "legal" turns out to be three different things wearing the same coat. Some of these are the Estatuto de los Trabajadores and cannot be bargained away.

Some are the sector agreement, which means they are real obligations that a different agreement could set differently — and several of the numbers people assume are law turn out to live here. And one or two are neither: decisions somebody made, which are the ones worth arguing about precisely because nobody has to keep them.

Every rule the roster obeysand whether it is law, bargaining, or a decision somebody made
Rulespainspain callcentreeu minimumBasisWhere it comes from
Hours an agent may be rostered in a week403948Estatuto de los TrabajadoresET art. 34.1 — forty hours averaged over the year
Longest single shift91012Estatuto de los TrabajadoresET art. 34.3 — nine hours of actual work, unless the agreement says otherwise
Shortest shift worth rostering444Modelling choiceNo legal minimum. Below this, travel time dominates the shift
Between the end of one shift and the start of the next121211Estatuto de los TrabajadoresET art. 34.3 — twelve hours. The rule that quietly shapes the whole roster
One uninterrupted break each week363624Estatuto de los TrabajadoresET art. 37.1 — a day and a half
Overtime permitted on top of the contracted week450Estatuto de los TrabajadoresET art. 35.2 caps overtime at eighty hours a year; this is a weekly working approximation of it
Days an agent may be rostered out of seven556Modelling choiceNot in the statute. Five of seven is the shape of a normal contract
Two blocks in one day with a gap between themnoyesnoCollective agreementLegal in Spain and common in contact centres. Its cost is measured rather than assumed
How far an agent may start from their own anchor hourunboundedunboundedunboundedModelling choiceNo legal basis at all — a promise to the people working the roster, and the only row here that is pure preference
The row that catches people out is the twelve hours between shifts. It is statute, it is unglamorous, and it does more to shape the week than the forty-hour limit does: it is what stops a late finish being followed by an early start, which is exactly the pattern a naive optimiser reaches for when demand peaks twice a day.

The search never got close enough for these to be prices. The worst row here stopped with a 99% optimality gap: the solver had a roster in hand and a proof that no roster could be better than a bound 99% away from it, and exhausted its budget in between. Only 0 of 8 rows were proved optimal. A difference of a few hundred euros between two rows is far inside that, so it says nothing about the rules. It also shows in the ordering: split shifts allowed, start times pinned to ±3h moved the wrong way against the baseline. A relaxation only ever adds legal rosters and a tightening only ever removes them, so at optimality neither can cross the baseline in that direction. One that did means the baseline — the row every other row is measured against — is itself the weaker solve. What is exact here is the shift count: how many legal shift patterns each rule admits is enumerated, not searched, so that column is the one to read.

What each rule buys, not what it costsone solve per row at 67 agents. The solver spends freedom on coverage, not on savings — so a looser rule reads as a dearer week that covers more of the curve
If this rule were relaxedLegal shiftsCoveragevs baselineSpare hoursWeek costsWhat it means
baseline — Spanish statute14599.38%33h€31,284ET arts. 34 and 37 as written
rest 12h → 11h14599.82%+0.44pp35h€31,388the EU Working Time Directive floor
weekly rest 36h → 24h14599.56%+0.18pp29h€31,269one day off instead of a day and a half
5 → 6 working days14599.60%+0.22pp49h€31,618shorter shifts spread across six days
split shifts allowed1,10599.16%?118h€32,243covered the wrong way against the baseline, which the feasible set forbids — the search ran out of time here, so this row says nothing about its rule
max shift 9h → 12h21799.74%+0.36pp131h€32,494long shifts, fewer handovers
40h → 48h week14599.91%+0.53pp24h€31,331the directive's absolute ceiling
start times pinned to ±3h14599.96%?44h€31,609covered the wrong way against the baseline, which the feasible set forbids — the search ran out of time here, so this row says nothing about its rule
The ones that do bite are worth the argument: weekly rest 36h → 24h. Read it as a question about today, not about hiring: this is one solve per row at 67 agents. The other question — the smallest team that could still cover the week — is what scripts/price_rules.py answers, and it takes about an hour.
Shifts to choose from145per agent per day, under spain
Agent-days to fill46967 agents × 7 days
Possible rosters10^1,013before a single rule is applied
Rules enforced9audited from the assignment, not the model

05Who works when

A CP-SAT model over one circular week, under the rules above.

The rosterone column per hour, Monday 00:00 on the left
Mon Tue Wed Thu Fri Sat Sun A1 A1: 9h from Mon 00:00 A1: 9h from Tue 00:00 A1: 9h from Wed 00:00 A1: 8h from Thu 00:00 A1: 4h from Fri 00:00 39h A2 A2: 9h from Mon 00:00 A2: 9h from Wed 00:00 A2: 4h from Fri 00:00 22h A3 A3: 9h from Mon 00:00 A3: 9h from Wed 00:00 A3: 9h from Thu 00:00 A3: 4h from Fri 00:00 31h A4 A4: 9h from Mon 00:00 A4: 9h from Tue 00:00 A4: 9h from Wed 00:00 A4: 9h from Thu 00:00 36h A5 A5: 9h from Mon 00:00 A5: 9h from Wed 00:00 A5: 9h from Thu 00:00 A5: 4h from Fri 00:00 31h A6 A6: 9h from Mon 00:00 A6: 9h from Tue 00:00 A6: 9h from Wed 00:00 A6: 9h from Thu 00:00 A6: 4h from Fri 11:00 40h A7 A7: 9h from Mon 05:00 A7: 9h from Tue 05:00 A7: 9h from Wed 05:00 A7: 9h from Thu 06:00 A7: 4h from Fri 04:00 40h A8 A8: 9h from Mon 06:00 A8: 9h from Tue 06:00 A8: 9h from Wed 06:00 A8: 9h from Thu 06:00 A8: 4h from Fri 04:00 40h A9 A9: 9h from Mon 06:00 A9: 9h from Tue 06:00 A9: 6h from Wed 06:00 A9: 9h from Thu 06:00 A9: 4h from Fri 08:00 37h A10 A10: 9h from Mon 07:00 A10: 9h from Tue 07:00 A10: 9h from Wed 07:00 A10: 9h from Thu 07:00 A10: 4h from Fri 04:00 40h A11 A11: 9h from Mon 07:00 A11: 9h from Tue 07:00 A11: 9h from Wed 07:00 A11: 9h from Thu 07:00 A11: 4h from Fri 04:00 40h A12 A12: 9h from Mon 07:00 A12: 9h from Tue 07:00 A12: 9h from Wed 08:00 A12: 9h from Thu 08:00 A12: 4h from Fri 05:00 40h A13 A13: 8h from Mon 07:00 A13: 9h from Tue 07:00 A13: 9h from Wed 08:00 A13: 8h from Thu 10:00 A13: 4h from Fri 06:00 38h A14 A14: 9h from Mon 08:00 A14: 9h from Tue 08:00 A14: 9h from Wed 08:00 A14: 4h from Thu 08:00 A14: 4h from Fri 06:00 35h A15 A15: 8h from Mon 08:00 A15: 9h from Tue 08:00 A15: 9h from Wed 08:00 A15: 9h from Thu 08:00 A15: 4h from Fri 06:00 39h A16 A16: 9h from Mon 08:00 A16: 8h from Tue 08:00 A16: 8h from Wed 08:00 A16: 4h from Thu 08:00 A16: 5h from Fri 07:00 34h A17 A17: 9h from Mon 08:00 A17: 9h from Tue 08:00 A17: 9h from Wed 09:00 A17: 9h from Thu 08:00 A17: 4h from Fri 07:00 40h A18 A18: 9h from Mon 09:00 A18: 9h from Tue 09:00 A18: 9h from Wed 09:00 A18: 9h from Thu 09:00 36h A19 A19: 9h from Mon 09:00 A19: 9h from Tue 09:00 A19: 9h from Thu 09:00 A19: 4h from Fri 08:00 31h A20 A20: 9h from Mon 09:00 A20: 9h from Tue 09:00 A20: 9h from Wed 09:00 A20: 9h from Thu 09:00 A20: 4h from Fri 08:00 40h A21 A21: 9h from Mon 09:00 A21: 9h from Tue 09:00 A21: 9h from Wed 09:00 A21: 9h from Thu 09:00 A21: 4h from Fri 08:00 40h A22 A22: 9h from Mon 09:00 A22: 9h from Tue 09:00 A22: 9h from Wed 09:00 A22: 9h from Thu 09:00 A22: 4h from Fri 08:00 40h A23 A23: 8h from Mon 09:00 A23: 9h from Tue 09:00 A23: 9h from Wed 09:00 A23: 9h from Thu 09:00 A23: 5h from Fri 08:00 40h A24 A24: 9h from Mon 09:00 A24: 9h from Tue 09:00 A24: 9h from Wed 09:00 A24: 9h from Thu 09:00 A24: 4h from Fri 08:00 40h A25 A25: 9h from Mon 09:00 A25: 9h from Tue 09:00 A25: 9h from Wed 09:00 A25: 8h from Thu 09:00 A25: 4h from Fri 08:00 39h A26 A26: 9h from Mon 09:00 A26: 9h from Tue 09:00 A26: 9h from Wed 09:00 A26: 9h from Thu 09:00 A26: 4h from Fri 08:00 40h A27 A27: 9h from Mon 09:00 A27: 8h from Tue 09:00 A27: 9h from Wed 10:00 A27: 9h from Thu 10:00 A27: 4h from Fri 08:00 39h A28 A28: 4h from Mon 09:00 A28: 9h from Tue 15:00 A28: 9h from Wed 12:00 A28: 9h from Thu 15:00 A28: 4h from Fri 12:00 35h A29 A29: 9h from Mon 15:00 A29: 9h from Tue 15:00 A29: 9h from Wed 15:00 A29: 9h from Thu 17:00 A29: 4h from Fri 14:00 40h A30 A30: 9h from Mon 15:00 A30: 9h from Tue 16:00 A30: 9h from Wed 15:00 A30: 8h from Thu 17:00 A30: 4h from Fri 14:00 39h A31 A31: 9h from Mon 16:00 A31: 9h from Tue 16:00 A31: 8h from Wed 16:00 A31: 7h from Thu 17:00 A31: 4h from Fri 14:00 37h A32 A32: 6h from Mon 16:00 A32: 9h from Tue 16:00 A32: 9h from Wed 17:00 A32: 4h from Thu 17:00 A32: 9h from Fri 09:00 37h A33 A33: 9h from Mon 17:00 A33: 9h from Tue 17:00 A33: 9h from Wed 17:00 A33: 4h from Thu 17:00 A33: 9h from Fri 09:00 40h A34 A34: 8h from Mon 17:00 A34: 4h from Tue 17:00 A34: 9h from Wed 17:00 A34: 9h from Fri 09:00 A34: 9h from Sat 06:00 39h A35 A35: 8h from Mon 18:00 A35: 4h from Tue 18:00 A35: 9h from Fri 09:00 A35: 9h from Sat 06:00 A35: 9h from Sun 03:00 39h A36 A36: 9h from Mon 09:00 A36: 9h from Tue 18:00 A36: 9h from Sat 06:00 A36: 9h from Sun 03:00 36h A37 A37: 6h from Mon 18:00 A37: 8h from Tue 18:00 A37: 9h from Fri 10:00 A37: 9h from Sat 07:00 A37: 8h from Sun 04:00 40h A38 A38: 4h from Mon 18:00 A38: 9h from Wed 18:00 A38: 9h from Fri 10:00 A38: 8h from Sat 08:00 A38: 9h from Sun 04:00 39h A39 A39: 4h from Mon 18:00 A39: 8h from Thu 11:00 A39: 9h from Fri 10:00 A39: 9h from Sat 07:00 A39: 9h from Sun 04:00 39h A40 A40: 6h from Mon 18:00 A40: 6h from Tue 18:00 A40: 9h from Fri 10:00 A40: 9h from Sat 07:00 A40: 9h from Sun 04:00 39h A41 A41: 6h from Tue 18:00 A41: 6h from Wed 18:00 A41: 9h from Fri 10:00 A41: 9h from Sat 07:00 A41: 9h from Sun 04:00 39h A42 A42: 6h from Wed 18:00 A42: 4h from Thu 18:00 A42: 9h from Fri 11:00 A42: 9h from Sat 08:00 A42: 9h from Sun 07:00 37h A43 A43: 9h from Wed 08:00 A43: 4h from Thu 18:00 A43: 9h from Fri 11:00 A43: 5h from Sat 10:00 A43: 8h from Sun 07:00 35h A44 A44: 6h from Wed 18:00 A44: 6h from Thu 18:00 A44: 9h from Fri 12:00 A44: 9h from Sat 09:00 A44: 9h from Sun 08:00 39h A45 A45: 4h from Mon 10:00 A45: 9h from Thu 18:00 A45: 9h from Fri 18:00 A45: 9h from Sat 15:00 A45: 9h from Sun 12:00 40h A46 A46: 6h from Tue 18:00 A46: 6h from Thu 18:00 A46: 8h from Fri 18:00 A46: 9h from Sat 15:00 A46: 9h from Sun 12:00 38h A47 A47: 6h from Wed 18:00 A47: 6h from Thu 18:00 A47: 9h from Fri 18:00 A47: 9h from Sat 15:00 A47: 9h from Sun 12:00 39h A48 A48: 6h from Mon 18:00 A48: 6h from Thu 18:00 A48: 7h from Fri 18:00 A48: 9h from Sat 15:00 A48: 9h from Sun 12:00 37h A49 A49: 9h from Mon 18:00 A49: 4h from Wed 09:00 A49: 9h from Fri 18:00 A49: 9h from Sat 15:00 A49: 9h from Sun 12:00 40h A50 A50: 9h from Tue 10:00 A50: 4h from Wed 09:00 A50: 7h from Fri 18:00 A50: 9h from Sat 15:00 A50: 9h from Sun 12:00 38h A51 A51: 4h from Tue 09:00 A51: 7h from Thu 22:00 A51: 9h from Fri 19:00 A51: 9h from Sat 16:00 A51: 9h from Sun 13:00 38h A52 A52: 4h from Mon 15:00 A52: 9h from Wed 10:00 A52: 9h from Fri 19:00 A52: 9h from Sat 16:00 A52: 9h from Sun 13:00 40h A53 A53: 4h from Mon 20:00 A53: 4h from Tue 20:00 A53: 7h from Fri 19:00 A53: 9h from Sat 16:00 A53: 9h from Sun 13:00 33h A54 A54: 9h from Thu 08:00 A54: 6h from Fri 20:00 A54: 9h from Sat 17:00 A54: 9h from Sun 14:00 33h A55 A55: 9h from Thu 08:00 A55: 9h from Fri 20:00 A55: 9h from Sat 18:00 A55: 9h from Sun 15:00 36h A56 A56: 2h from Mon 00:00 A56: 9h from Tue 00:00 A56: 6h from Thu 20:00 A56: 9h from Fri 23:00 A56: 7h from Sat 22:00 A56: 7h from Sun 17:00 40h A57 A57: 3h from Mon 00:00 A57: 4h from Fri 00:00 A57: 9h from Sat 03:00 A57: 3h from Sun 21:00 19h A58 A58: 2h from Mon 00:00 A58: 9h from Tue 00:00 A58: 9h from Thu 00:00 A58: 4h from Fri 11:00 A58: 7h from Sat 03:00 A58: 3h from Sun 21:00 34h A59 A59: 1h from Mon 00:00 A59: 4h from Thu 20:00 A59: 5h from Fri 12:00 A59: 5h from Sat 23:00 A59: 3h from Sun 21:00 18h A60 A60: 1h from Mon 00:00 A60: 5h from Fri 12:00 A60: 5h from Sat 23:00 A60: 3h from Sun 21:00 14h A61 A61: 4h from Fri 04:00 A61: 6h from Sat 00:00 A61: 7h from Sun 09:00 17h A62 A62: 7h from Sat 00:00 A62: 4h from Sun 00:00 11h A63 A63: 8h from Tue 01:00 A63: 5h from Fri 09:00 A63: 4h from Sat 02:00 A63: 4h from Sun 00:00 21h A64 A64: 4h from Fri 10:00 A64: 4h from Sat 02:00 A64: 4h from Sun 00:00 12h A65 A65: 4h from Fri 10:00 A65: 4h from Sat 08:00 A65: 4h from Sun 00:00 12h A66 A66: 9h from Fri 09:00 A66: 4h from Sun 00:00 13h A67 A67: 6h from Wed 18:00 A67: 7h from Sat 12:00 A67: 4h from Sun 20:00 17h
dayincludes night hoursclick any row for that agent’s week
Coverage against requirementevery cell is the signed difference from what the hour needed: +0 is exactly covered, −X is orange and short of it, +X is blue and more than it
Mon Tue Wed Thu Fri Sat Sun 00 Mon 00:00 — 11 vs 11 needed (+0, exact) +0 Tue 00:00 — 10 vs 10 needed (+0, exact) +0 Wed 00:00 — 12 vs 10 needed (+2, spare) +2 Thu 00:00 — 10 vs 10 needed (+0, exact) +0 Fri 00:00 — 10 vs 10 needed (+0, exact) +0 Sat 00:00 — 14 vs 14 needed (+0, exact) +0 Sun 00:00 — 13 vs 15 needed (-2, short) -2 Mon 01:00 — 9 vs 9 needed (+0, exact) +0 Tue 01:00 — 9 vs 9 needed (+0, exact) +0 Wed 01:00 — 9 vs 9 needed (+0, exact) +0 Thu 01:00 — 10 vs 9 needed (+1, spare) +1 Fri 01:00 — 9 vs 9 needed (+0, exact) +0 Sat 01:00 — 12 vs 12 needed (+0, exact) +0 Sun 01:00 — 10 vs 14 needed (-4, short) -4 Mon 02:00 — 7 vs 7 needed (+0, exact) +0 Tue 02:00 — 7 vs 7 needed (+0, exact) +0 Wed 02:00 — 7 vs 7 needed (+0, exact) +0 Thu 02:00 — 7 vs 7 needed (+0, exact) +0 Fri 02:00 — 7 vs 7 needed (+0, exact) +0 Sat 02:00 — 11 vs 11 needed (+0, exact) +0 Sun 02:00 — 9 vs 11 needed (-2, short) -2 03 Mon 03:00 — 6 vs 6 needed (+0, exact) +0 Tue 03:00 — 6 vs 6 needed (+0, exact) +0 Wed 03:00 — 6 vs 6 needed (+0, exact) +0 Thu 03:00 — 6 vs 6 needed (+0, exact) +0 Fri 03:00 — 6 vs 6 needed (+0, exact) +0 Sat 03:00 — 10 vs 9 needed (+1, spare) +1 Sun 03:00 — 10 vs 10 needed (+0, exact) +0 Mon 04:00 — 6 vs 6 needed (+0, exact) +0 Tue 04:00 — 6 vs 6 needed (+0, exact) +0 Wed 04:00 — 6 vs 6 needed (+0, exact) +0 Thu 04:00 — 6 vs 6 needed (+0, exact) +0 Fri 04:00 — 6 vs 6 needed (+0, exact) +0 Sat 04:00 — 8 vs 7 needed (+1, spare) +1 Sun 04:00 — 8 vs 8 needed (+0, exact) +0 Mon 05:00 — 7 vs 7 needed (+0, exact) +0 Tue 05:00 — 7 vs 7 needed (+0, exact) +0 Wed 05:00 — 7 vs 7 needed (+0, exact) +0 Thu 05:00 — 6 vs 6 needed (+0, exact) +0 Fri 05:00 — 6 vs 6 needed (+0, exact) +0 Sat 05:00 — 7 vs 7 needed (+0, exact) +0 Sun 05:00 — 7 vs 7 needed (+0, exact) +0 06 Mon 06:00 — 9 vs 9 needed (+0, exact) +0 Tue 06:00 — 9 vs 9 needed (+0, exact) +0 Wed 06:00 — 9 vs 9 needed (+0, exact) +0 Thu 06:00 — 9 vs 9 needed (+0, exact) +0 Fri 06:00 — 9 vs 9 needed (+0, exact) +0 Sat 06:00 — 7 vs 7 needed (+0, exact) +0 Sun 06:00 — 7 vs 7 needed (+0, exact) +0 Mon 07:00 — 13 vs 13 needed (+0, exact) +0 Tue 07:00 — 13 vs 13 needed (+0, exact) +0 Wed 07:00 — 11 vs 11 needed (+0, exact) +0 Thu 07:00 — 11 vs 11 needed (+0, exact) +0 Fri 07:00 — 11 vs 11 needed (+0, exact) +0 Sat 07:00 — 10 vs 9 needed (+1, spare) +1 Sun 07:00 — 9 vs 9 needed (+0, exact) +0 Mon 08:00 — 17 vs 17 needed (+0, exact) +0 Tue 08:00 — 17 vs 17 needed (+0, exact) +0 Wed 08:00 — 17 vs 17 needed (+0, exact) +0 Thu 08:00 — 17 vs 17 needed (+0, exact) +0 Fri 08:00 — 16 vs 16 needed (+0, exact) +0 Sat 08:00 — 12 vs 10 needed (+2, spare) +2 Sun 08:00 — 10 vs 10 needed (+0, exact) +0 09 Mon 09:00 — 23 vs 23 needed (+0, exact) +0 Tue 09:00 — 22 vs 21 needed (+1, spare) +1 Wed 09:00 — 22 vs 20 needed (+2, spare) +2 Thu 09:00 — 21 vs 20 needed (+1, spare) +1 Fri 09:00 — 21 vs 20 needed (+1, spare) +1 Sat 09:00 — 13 vs 11 needed (+2, spare) +2 Sun 09:00 — 11 vs 11 needed (+0, exact) +0 Mon 10:00 — 24 vs 24 needed (+0, exact) +0 Tue 10:00 — 23 vs 23 needed (+0, exact) +0 Wed 10:00 — 24 vs 24 needed (+0, exact) +0 Thu 10:00 — 23 vs 21 needed (+2, spare) +2 Fri 10:00 — 25 vs 22 needed (+3, spare) +3 Sat 10:00 — 13 vs 13 needed (+0, exact) +0 Sun 10:00 — 11 vs 14 needed (-3, short) -3 Mon 11:00 — 24 vs 24 needed (+0, exact) +0 Tue 11:00 — 23 vs 23 needed (+0, exact) +0 Wed 11:00 — 24 vs 24 needed (+0, exact) +0 Thu 11:00 — 24 vs 24 needed (+0, exact) +0 Fri 11:00 — 28 vs 22 needed (+6, spare) +6 Sat 11:00 — 13 vs 13 needed (+0, exact) +0 Sun 11:00 — 11 vs 14 needed (-3, short) -3 12 Mon 12:00 — 24 vs 23 needed (+1, spare) +1 Tue 12:00 — 23 vs 23 needed (+0, exact) +0 Wed 12:00 — 24 vs 24 needed (+0, exact) +0 Thu 12:00 — 22 vs 22 needed (+0, exact) +0 Fri 12:00 — 22 vs 22 needed (+0, exact) +0 Sat 12:00 — 12 vs 13 needed (-1, short) -1 Sun 12:00 — 14 vs 14 needed (+0, exact) +0 Mon 13:00 — 23 vs 23 needed (+0, exact) +0 Tue 13:00 — 22 vs 21 needed (+1, spare) +1 Wed 13:00 — 22 vs 22 needed (+0, exact) +0 Thu 13:00 — 22 vs 22 needed (+0, exact) +0 Fri 13:00 — 21 vs 21 needed (+0, exact) +0 Sat 13:00 — 12 vs 14 needed (-2, short) -2 Sun 13:00 — 13 vs 13 needed (+0, exact) +0 Mon 14:00 — 21 vs 21 needed (+0, exact) +0 Tue 14:00 — 21 vs 20 needed (+1, spare) +1 Wed 14:00 — 21 vs 21 needed (+0, exact) +0 Thu 14:00 — 22 vs 21 needed (+1, spare) +1 Fri 14:00 — 21 vs 21 needed (+0, exact) +0 Sat 14:00 — 12 vs 14 needed (-2, short) -2 Sun 14:00 — 14 vs 14 needed (+0, exact) +0 15 Mon 15:00 — 21 vs 21 needed (+0, exact) +0 Tue 15:00 — 21 vs 20 needed (+1, spare) +1 Wed 15:00 — 22 vs 21 needed (+1, spare) +1 Thu 15:00 — 20 vs 20 needed (+0, exact) +0 Fri 15:00 — 19 vs 19 needed (+0, exact) +0 Sat 15:00 — 14 vs 14 needed (+0, exact) +0 Sun 15:00 — 14 vs 14 needed (+0, exact) +0 Mon 16:00 — 19 vs 19 needed (+0, exact) +0 Tue 16:00 — 19 vs 19 needed (+0, exact) +0 Wed 16:00 — 20 vs 20 needed (+0, exact) +0 Thu 16:00 — 18 vs 17 needed (+1, spare) +1 Fri 16:00 — 18 vs 18 needed (+0, exact) +0 Sat 16:00 — 12 vs 12 needed (+0, exact) +0 Sun 16:00 — 12 vs 12 needed (+0, exact) +0 Mon 17:00 — 17 vs 17 needed (+0, exact) +0 Tue 17:00 — 17 vs 17 needed (+0, exact) +0 Wed 17:00 — 18 vs 17 needed (+1, spare) +1 Thu 17:00 — 17 vs 17 needed (+0, exact) +0 Fri 17:00 — 16 vs 16 needed (+0, exact) +0 Sat 17:00 — 12 vs 11 needed (+1, spare) +1 Sun 17:00 — 12 vs 12 needed (+0, exact) +0 18 Mon 18:00 — 14 vs 14 needed (+0, exact) +0 Tue 18:00 — 14 vs 14 needed (+0, exact) +0 Wed 18:00 — 15 vs 15 needed (+0, exact) +0 Thu 18:00 — 15 vs 15 needed (+0, exact) +0 Fri 18:00 — 14 vs 14 needed (+0, exact) +0 Sat 18:00 — 12 vs 12 needed (+0, exact) +0 Sun 18:00 — 12 vs 12 needed (+0, exact) +0 Mon 19:00 — 13 vs 13 needed (+0, exact) +0 Tue 19:00 — 13 vs 13 needed (+0, exact) +0 Wed 19:00 — 13 vs 13 needed (+0, exact) +0 Thu 19:00 — 13 vs 13 needed (+0, exact) +0 Fri 19:00 — 12 vs 12 needed (+0, exact) +0 Sat 19:00 — 11 vs 11 needed (+0, exact) +0 Sun 19:00 — 12 vs 11 needed (+1, spare) +1 Mon 20:00 — 14 vs 11 needed (+3, spare) +3 Tue 20:00 — 14 vs 12 needed (+2, spare) +2 Wed 20:00 — 13 vs 12 needed (+1, spare) +1 Thu 20:00 — 15 vs 12 needed (+3, spare) +3 Fri 20:00 — 12 vs 12 needed (+0, exact) +0 Sat 20:00 — 11 vs 11 needed (+0, exact) +0 Sun 20:00 — 13 vs 11 needed (+2, spare) +2 21 Mon 21:00 — 14 vs 11 needed (+3, spare) +3 Tue 21:00 — 13 vs 12 needed (+1, spare) +1 Wed 21:00 — 12 vs 12 needed (+0, exact) +0 Thu 21:00 — 13 vs 12 needed (+1, spare) +1 Fri 21:00 — 11 vs 11 needed (+0, exact) +0 Sat 21:00 — 11 vs 11 needed (+0, exact) +0 Sun 21:00 — 11 vs 11 needed (+0, exact) +0 Mon 22:00 — 11 vs 11 needed (+0, exact) +0 Tue 22:00 — 12 vs 12 needed (+0, exact) +0 Wed 22:00 — 12 vs 12 needed (+0, exact) +0 Thu 22:00 — 12 vs 11 needed (+1, spare) +1 Fri 22:00 — 11 vs 11 needed (+0, exact) +0 Sat 22:00 — 12 vs 12 needed (+0, exact) +0 Sun 22:00 — 8 vs 11 needed (-3, short) -3 Mon 23:00 — 11 vs 11 needed (+0, exact) +0 Tue 23:00 — 12 vs 12 needed (+0, exact) +0 Wed 23:00 — 12 vs 12 needed (+0, exact) +0 Thu 23:00 — 12 vs 12 needed (+0, exact) +0 Fri 23:00 — 12 vs 12 needed (+0, exact) +0 Sat 23:00 — 14 vs 14 needed (+0, exact) +0 Sun 23:00 — 7 vs 12 needed (-5, short) -5
−X short of the requirement+0 exactly covered+X more on the floor than needed

How it got there

CP-SAT does not walk to an answer, it closes on one from both sides. A portfolio of eight workers proposes rosters from above while a bound climbs from below, and the search is finished when the two meet. On this week it found 23 successively better rosters in 60 seconds and never did meet the bound — which is normal, and the gap it stopped at is the honest measure of how much is still unknown.

The search, second by secondpenalty score against wall-clock seconds — the roster it has falls, the proof it has rises, and they never touch
286,423 285,560 284,698 Best roster found — it can only fall 1,499 749 0 Best proof — no roster can score below this 27.0s — best roster 286,195 27.7s — best roster 286,184 32.8s — best roster 285,672 33.3s — best roster 285,516 35.8s — best roster 285,494 37.3s — best roster 285,483 40.2s — best roster 285,417 41.2s — best roster 285,388 41.6s — best roster 285,376 45.0s — best roster 285,332 45.6s — best roster 285,326 47.2s — best roster 285,299 47.3s — best roster 285,211 48.4s — best roster 285,205 51.5s — best roster 285,154 52.8s — best roster 285,065 52.8s — best roster 285,059 54.6s — best roster 285,015 55.5s — best roster 285,003 55.5s — best roster 284,959 57.7s — best roster 284,948 58.0s — best roster 284,937 58.0s — best roster 284,926 0s 12s 24s 36s 48s 60s seconds of search — budget 60s unproven: 283,656 of 284,926 — the two strips never met
best roster found (incumbent)best proof so far (lower bound)each strip is framed on its own values, not on zero — the two are 224x apart and cannot share a scale
Model size68,005boolean variables, 8 search workers
Rosters found23each better than the last
First at27.0sobjective 286,195
Improvement0%off the first legal week it found
Gap left100%what the clock did not resolve
The blue line is the best week found so far and the green one is the proof that nothing cheaper than that value exists. Blue falls quickly — most of the 0% is gone early — and then crawls, which is the usual shape: the easy savings are the obvious ones. Green barely moves, and that is the real story. Proving a roster optimal is far harder than finding a good one, so the gap stays wide even though the roster stopped improving. The word optimal is not available here and is not used.
Agents67spain rules
Coverage98.8%27h short
Spare53hpaid and not needed
Rulesall respectedaudited from the assignment

06Move the numbers yourself

The same pipeline, running in your browser. Change how many people you have, or what you promise them, and watch the matrix rebuild.

Everything above is one scenario. The panel below is the whole thing — Erlang C, the shift catalogue, the rules audit and the cost model — ported to JavaScript and running on the page, so the question "what if we were four people short" takes a few milliseconds instead of a terminal.

This is the greedy roster, not the solver. CP-SAT does not run in a browser, so the panel builds each week the way the warm start does: hand every agent the shift that closes the biggest remaining hole, if the rules still hold. That is instant and it is legal — the audit runs live and will say so if it ever is not — but it leaves more spare hours than the solver. On this week the solver reached 53h spare; the greedy alone lands higher, and the difference is what the sixty seconds of search above bought.
people

36–90

sec

Toronto 311 measures 290 over 1.01M calls

% in 30s

NYC commits to 80%

%

Breaks, training, sickness, holiday

What the roster is allowed to do

The distribution matrixone column per hour, Monday 00:00 on the left — blue is daytime, amber includes night hours
day includes night hours
Agents neededErlang C on the forecast
On the floor against neededred short, blue spare
Every headcount from 36 to 90coverage in green, weekly cost in amber — the line marks where the slider is
coverage cost per week

07What it costs

Priced against the Spanish sector agreement, premium by premium.

An hour of rostered agent time costs €12.84: €17,139.58 a year over 1,764 rostered hours is €9.72 gross, and employer social security adds 32.15%.

Premiums go on the ordinary hour, not the loaded one, which is how the agreement writes them — and the other order quietly inflates every night shift by a third. Night work adds a flat amount per hour between 22:00 and 06:00, Sundays and holidays a flat amount per shift, and overtime a percentage on top.

One week of this rosterBlended €13.63 an hour, premiums included
quantity amount
Ordinary hours 2,296 h €29,481
Night premium 504 h €1,305
Sunday premium 33 shifts €506
Holiday premium 0 shifts €0
Overtime uplift 0 h €0
Total 2,296 h €31,292
Week€31,292all premiums in
Year€1.63Mat 52 weeks
Per contact€3.191over 9,807 contacts
Blended hour€13.63vs €12.84 base
Cost per agent, this weekall 67 rostered agents — every bar is the same grade, so the colour is the whole story
601 451 301 150 0 A1 — ordinary hours: 501 EUR A1 — night premium: 73 EUR A1 573 A2 — ordinary hours: 282 EUR A2 — night premium: 41 EUR A3 — ordinary hours: 398 EUR A3 — night premium: 57 EUR A4 — ordinary hours: 462 EUR A4 — night premium: 62 EUR A4 524 A5 — ordinary hours: 398 EUR A5 — night premium: 57 EUR A6 — ordinary hours: 514 EUR A6 — night premium: 62 EUR A7 — ordinary hours: 514 EUR A7 — night premium: 13 EUR A7 527 A8 — ordinary hours: 514 EUR A8 — night premium: 5 EUR A9 — ordinary hours: 475 EUR A10 — ordinary hours: 514 EUR A10 — night premium: 5 EUR A10 519 A11 — ordinary hours: 514 EUR A11 — night premium: 5 EUR A12 — ordinary hours: 514 EUR A12 — night premium: 3 EUR A13 — ordinary hours: 488 EUR A13 488 A14 — ordinary hours: 449 EUR A15 — ordinary hours: 501 EUR A16 — ordinary hours: 437 EUR A16 437 A17 — ordinary hours: 514 EUR A18 — ordinary hours: 462 EUR A19 — ordinary hours: 398 EUR A19 398 A20 — ordinary hours: 514 EUR A21 — ordinary hours: 514 EUR A22 — ordinary hours: 514 EUR A22 514 A23 — ordinary hours: 514 EUR A24 — ordinary hours: 514 EUR A25 — ordinary hours: 501 EUR A25 501 A26 — ordinary hours: 514 EUR A27 — ordinary hours: 501 EUR A28 — ordinary hours: 449 EUR A28 — night premium: 10 EUR A28 460 A29 — ordinary hours: 514 EUR A29 — night premium: 26 EUR A30 — ordinary hours: 501 EUR A30 — night premium: 26 EUR A31 — ordinary hours: 475 EUR A31 — night premium: 26 EUR A31 501 A32 — ordinary hours: 475 EUR A32 — night premium: 18 EUR A33 — ordinary hours: 514 EUR A33 — night premium: 31 EUR A34 — ordinary hours: 501 EUR A34 — night premium: 18 EUR A34 519 A35 — ordinary hours: 501 EUR A35 — night premium: 18 EUR A35 — Sunday premium: 15 EUR A36 — ordinary hours: 462 EUR A36 — night premium: 21 EUR A36 — Sunday premium: 15 EUR A37 — ordinary hours: 514 EUR A37 — night premium: 21 EUR A37 — Sunday premium: 15 EUR A37 550 A38 — ordinary hours: 501 EUR A38 — night premium: 18 EUR A38 — Sunday premium: 15 EUR A39 — ordinary hours: 501 EUR A39 — night premium: 5 EUR A39 — Sunday premium: 15 EUR A40 — ordinary hours: 501 EUR A40 — night premium: 16 EUR A40 — Sunday premium: 15 EUR A40 532 A41 — ordinary hours: 501 EUR A41 — night premium: 16 EUR A41 — Sunday premium: 15 EUR A42 — ordinary hours: 475 EUR A42 — night premium: 5 EUR A42 — Sunday premium: 15 EUR A43 — ordinary hours: 449 EUR A43 — Sunday premium: 15 EUR A43 465 A44 — ordinary hours: 501 EUR A44 — night premium: 10 EUR A44 — Sunday premium: 15 EUR A45 — ordinary hours: 514 EUR A45 — night premium: 31 EUR A45 — Sunday premium: 15 EUR A46 — ordinary hours: 488 EUR A46 — night premium: 26 EUR A46 — Sunday premium: 15 EUR A46 529 A47 — ordinary hours: 501 EUR A47 — night premium: 28 EUR A47 — Sunday premium: 15 EUR A48 — ordinary hours: 475 EUR A48 — night premium: 23 EUR A48 — Sunday premium: 15 EUR A49 — ordinary hours: 514 EUR A49 — night premium: 31 EUR A49 — Sunday premium: 15 EUR A49 560 A50 — ordinary hours: 488 EUR A50 — night premium: 13 EUR A50 — Sunday premium: 15 EUR A51 — ordinary hours: 488 EUR A51 — night premium: 41 EUR A51 — Sunday premium: 15 EUR A52 — ordinary hours: 514 EUR A52 — night premium: 23 EUR A52 — Sunday premium: 15 EUR A52 552 A53 — ordinary hours: 424 EUR A53 — night premium: 28 EUR A53 — Sunday premium: 15 EUR A54 — ordinary hours: 424 EUR A54 — night premium: 23 EUR A54 — Sunday premium: 15 EUR A55 — ordinary hours: 462 EUR A55 — night premium: 36 EUR A55 — Sunday premium: 15 EUR A55 514 A56 — ordinary hours: 514 EUR A56 — night premium: 73 EUR A56 — Sunday premium: 15 EUR A57 — ordinary hours: 244 EUR A57 — night premium: 31 EUR A57 — Sunday premium: 15 EUR A58 — ordinary hours: 437 EUR A58 — night premium: 49 EUR A58 — Sunday premium: 15 EUR A58 501 A59 — ordinary hours: 231 EUR A59 — night premium: 26 EUR A59 — Sunday premium: 15 EUR A60 — ordinary hours: 180 EUR A60 — night premium: 21 EUR A60 — Sunday premium: 15 EUR A61 — ordinary hours: 218 EUR A61 — night premium: 21 EUR A61 — Sunday premium: 15 EUR A61 254 A62 — ordinary hours: 141 EUR A62 — night premium: 26 EUR A62 — Sunday premium: 15 EUR A63 — ordinary hours: 270 EUR A63 — night premium: 34 EUR A63 — Sunday premium: 15 EUR A64 — ordinary hours: 154 EUR A64 — night premium: 21 EUR A64 — Sunday premium: 15 EUR A64 190 A65 — ordinary hours: 154 EUR A65 — night premium: 10 EUR A65 — Sunday premium: 15 EUR A66 — ordinary hours: 167 EUR A66 — night premium: 10 EUR A66 — Sunday premium: 15 EUR A67 — ordinary hours: 218 EUR A67 — night premium: 10 EUR A67 — Sunday premium: 15 EUR A67 244
overtime holiday premium Sunday premium night premium ordinary hours
Everyone here is on the same grade, so the blue is identical work at an identical rate and every band above it is a scheduling decision. 50 agents carry night hours (€1,305 across the week), 33 work a Sunday (€506), and nobody goes into overtime, which is why there is no pink: the solver prices an overtime hour at a hundred times an ordinary one and will restructure the whole week to avoid a single one. The gap between the cheapest agent at €180 and the dearest at €601 is entirely those bands.

08Where every number came from

Including the two that were looked for and not found.

Every number that came from outside this repositorySorted as declared. 'assumed' means no published figure was found. Every source with a published page links straight to it.
what confidence source value
average handle time on a general-purpose municipal 311 phone line published City of Toronto, 311 Toronto Annual Report 2025 — 4:50 over ~1.01M calls ↗ 290.00 seconds
share of calls NYC 311 commits to answering within the target published NYC Mayor's Management Report FY2025 — target 80%, actual 80% ↗ 0.80 share
the answer threshold NYC 311 commits to published NYC Mayor's Management Report FY2025. Toronto commits to 75s, San Francisco to 60s ↗ 30.00 seconds
average wait before reaching a tier-1 agent, NYC 311, FY2025 published NYC Mayor's Management Report FY2025 — 0:25 against a 0:30 target ↗ 25.00 seconds
calls to NYC 311 in Fiscal 2025 published NYC Mayor's Management Report FY2025, '311 calls (000)' = 17,377 ↗ 17,377,000 calls/year
calls to NYC 311's Spanish-language line in Fiscal 2025 published NYC Mayor's Management Report FY2025, '311 Spanish language calls (000)' = 484 ↗ 484,000 calls/year
service requests completed by NYC 311 in Fiscal 2025 published NYC Mayor's Management Report FY2025, 'Completed service requests (000)' = 3,766 ↗ 3,766,000 requests/year
311-NYC text message contacts in Fiscal 2025 published NYC Mayor's Management Report FY2025, '311-NYC (text) contacts (000)' = 270 ↗ 270,000 contacts/year
handling time for one deferred service request assumed No public-sector figure found. Eight minutes is an assumption, exposed as a parameter 480.00 seconds
share of deferred transactions processed within the targeted cycle time published COPC CX Standard Release 7.0 (2021), Exhibit 1, 'Human Assisted Deferred Transactions' — best-practice target for the 'On Time' metric ↗ 0.95 share
customer compose time divided by agent compose time, the one input the chat concurrency model needs derived US 8,064,589 B2 (Lewis & Beshears, 2011), 'Estimating number of agents for multiple chat channels' ↗ 1.00 ratio
gross annual salary, Nivel 10 (Teleoperador/a), 2026 table published III Convenio colectivo estatal del sector de contact center, 2026 salary tables — BOE-A-2026-5300 ↗ 17,140 EUR/year
maximum ordinary annual working time published III Convenio colectivo estatal del sector de contact center, art. 22 (Jornada) — 1,764 h/year, 39 h/week of effective work ↗ 1,764 hours/year
employer social security contribution in Spain published 23.60% common contingencies + 5.50% unemployment + 1.50% AT/EP + 0.75% MEI + 0.60% vocational training + 0.20% FOGASA ↗ 0.32 share of gross
night work premium, flat per hour worked between 22:00 and 06:00 published III Convenio colectivo estatal del sector de contact center, plus de nocturnidad (2026 table) ↗ 1.96 EUR/hour
premium for a shift worked on a Sunday published III Convenio colectivo estatal del sector de contact center, plus de domingo (2026 table) ↗ 15.33 EUR/day
premium for a shift worked on a normal public holiday published III Convenio colectivo estatal del sector de contact center, plus de festivo (2026 table). Special holidays carry €94.39 ↗ 44.51 EUR/day
uplift on the ordinary hour for daytime overtime published III Convenio colectivo estatal del sector de contact center. Night overtime +60%, holiday +60%, holiday night +80% ↗ 0.25 share
The weakest input is handling time for one deferred service request. There is no published handle time for email or ticket work in a public-sector contact centre, so eight minutes is an assumption and the whole deferred-work column moves with it. New York publishes no handle time either, which is why the voice figure is Toronto's.