Open-Source Library

PyWorkforce

Plan staffing with Erlang-C and assign shifts under demand and scheduling constraints using Python. Examples for call centers and operations teams.

PyWorkforce
Conceptual project illustration

The Problem It Solves

Operations teams at call centers and service organizations make staffing decisions every week that directly determine service quality and labor costs. The question is simple to state: how many agents do you need, on which shifts, with what skills?

In practice, most teams answer it with spreadsheets and intuition. The spreadsheet tells them last week's headcount. The intuition adjusts for seasonality. The result is a schedule that over-staffs some hours and under-staffs others, and a planning process that can't be audited, repeated, or improved systematically.

The mathematics to do this rigorously has existed for decades: Erlang C for queue modeling, linear programming for shift scheduling, integer programming for rostering. What was missing was a Python library that composed these into a usable, production-ready toolkit.

What I Built

pyworkforce implements the full workforce management pipeline in three composable modules:

Queue modeling answers: given expected call volume and handling time, how many agents are needed to meet a service level target? The Erlang C formula is the industry standard; the library wraps it with configurable parameters including shrinkage (agent non-productive time) and occupancy caps.

Shift scheduling answers: given per-hour staffing requirements, which shifts should run and how many agents on each? This is formulated as a linear programming problem: minimize the gap between required and assigned headcount, given a set of available shift templates.

Rostering answers: given the required shift assignments, which specific agent works which shift sequence? This is a combinatorial problem with hard constraints: maximum consecutive days, minimum rest between shifts, skill requirements, days-off patterns.

Each module can be used independently or chained into a full pipeline.

Read the objective and solver status

An optimization result applies to the objective and constraints you supplied. In the example below, MinAbsDifference minimizes the total absolute difference between required and assigned staffing. It does not promise full coverage or the fewest possible agents.

An OPTIMAL status means the solver proved optimality for that model; FEASIBLE indicates a solution without that proof. If full coverage is required, consider MinRequiredResources and check the returned status and coverage. The scheduling documentation describes the distinct objectives.

Infeasibility, time limits and invalid inputs need separate handling. A solver result also depends on the demand forecast and the queueing assumptions: an optimal schedule for the model does not guarantee the observed service level.

The Pipeline in Practice

The three modules compose naturally. A call center running this end-to-end would:

python
from pyworkforce.queuing import ErlangC
from pyworkforce.scheduling import MinAbsDifference
from pyworkforce.rostering import MinHoursRoster

# Step 1: how many agents per hour to hit the service level?
erlang = ErlangC(
    transactions=120,   # calls arriving in the interval
    interval=60,        # interval length (minutes)
    aht=4,              # average handling time (minutes)
    asa=20/60,          # target answer speed (minutes)
    shrinkage=0.30,     # non-productive agent time
)
positions_per_hour = erlang.required_positions(
    service_level=0.80,
    max_occupancy=0.85,
)["positions"]

# Step 2: which shifts cover that demand, and how many of each?
scheduler = MinAbsDifference(
    num_days=7,
    periods=24,
    shifts_coverage={
        "Morning":   [0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0],
        "Afternoon": [0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0],
        "Night":     [1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1],
    },
    required_resources=[[positions_per_hour] * 24] * 7,
    max_period_concurrency=positions_per_hour,
    max_shift_concurrency=positions_per_hour,
)
shift_plan = scheduler.solve()

# Step 3: assign individual agents to shift sequences
roster = MinHoursRoster(
    num_days=7,
    resources=[f"agent_{i}" for i in range(25)],
    shifts=["Morning", "Afternoon", "Night"],
    shifts_hours=[8, 8, 8],
    min_working_hours=40,
    max_resting=2,
    required_resources={
        "Morning":   [positions_per_hour] * 7,
        "Afternoon": [positions_per_hour] * 7,
        "Night":     [positions_per_hour] * 7,
    },
)
weekly_roster = roster.solve()

From call volume to individual weekly schedule: with explicit assumptions, constraints and solver statuses at each step.

Key Design Decisions

The shrinkage parameter in the Erlang C module deserves attention. Raw Erlang C gives you the number of agents needed assuming they are available 100% of the time. Real agents take breaks, attend training, handle admin tasks, and call in sick. Ignoring shrinkage typically underestimates headcount by 25–35%, which is exactly why spreadsheet-based plans chronically under-staff.

The scheduling objective: MinAbsDifference: minimizes the absolute difference between required and scheduled headcount across all periods, rather than just minimizing total cost. This produces balanced coverage rather than solutions that sacrifice certain hours to hit a numeric target.

The rostering module models constraints as hard constraints in the integer program. There is no penalty weighting, no soft constraint relaxation, no post-processing. A schedule either respects the constraints or the solver returns infeasible.

Where It Stands

pyworkforce has been actively developed since 2021 and is currently at v0.5.4. It is published on PyPI, documented at pyworkforce.rodrigo-arenas.com, and built on top of Google OR-Tools.

It was built from direct work on staffing optimization problems for operations teams: the kind of engagement where a spreadsheet answer and a mathematical answer diverge by enough to matter.

About the author

Rodrigo Arenas is a software architect and machine learning engineer in Medellín, Colombia. He builds products, platforms, and open-source software for AI, Data Engineering, and Machine Learning: creator of Ciaren, sklearn-genetic-opt, and PyWorkforce.

Let’s put your idea to work.

If you’re building an AI or data platform, share the context of your project.

Back to projects