Skip to main content

Tests Codecov PyPI Version Python Version Docs

pyworkforce

Workforce management, made practical. Covers the full planning pipeline from queue staffing to named rosters and break scheduling — built on Erlang queuing theory and OR-Tools constraint programming, with a scikit-learn-style API.

Geared toward contact / call centres, but the same techniques apply to hospitals, retail, logistics and any operation matching variable demand to finite resources.

📖 Full documentation: pyworkforce.rodrigo-arenas.com


Installation

pip install pyworkforce

Requires Python 3.12 – 3.14. conda-forge support is pending an update to the ortools-python feedstock for Python 3.12.


The planning workflow

pyworkforce covers five sequential planning steps:

Planning workflow

Step Module Key classes Question answered
1 pyworkforce.queuing ErlangC, ErlangA, ErlangB How many agents / channels are needed?
2 pyworkforce.staffing MultiSkillStaffing What is the optimal skill-profile mix?
3 pyworkforce.scheduling MinRequiredResources, MinAbsDifference How many agents per shift?
4 pyworkforce.rostering MinHoursRoster Who works on which day and shift?
5 pyworkforce.breaks BreakScheduler When do individual breaks happen?

1 — Queuing

Estimate the number of agents (or channels) needed to meet a service target given an incoming demand.

Erlang C — infinite patience

The classic M/M/c queue: Poisson arrivals, exponential handling times, unlimited waiting room.

Erlang C queue system

from pyworkforce.queuing import ErlangC

erlang = ErlangC(transactions=100, aht=3, asa=20/60, interval=30, shrinkage=0.3)

result = erlang.required_positions(service_level=0.8, max_occupancy=0.85)
print(result)
{'raw_positions': 14,
 'positions': 20,
 'service_level': 0.8884,
 'occupancy': 0.7143,
 'waiting_probability': 0.1741}

Erlang A — finite patience (abandonment)

Adds a patience parameter: callers who wait too long hang up. Typically requires fewer agents than Erlang C.

Erlang A queue system — with abandonment

from pyworkforce.queuing import ErlangA

erlang = ErlangA(transactions=100, aht=3, asa=20/60, interval=30,
                 patience=5, shrinkage=0.3)

result = erlang.required_positions(service_level=0.8, max_occupancy=0.85,
                                   max_abandonment=0.05)
print(result)
{'raw_positions': 13,
 'positions': 19,
 'service_level': 0.8584,
 'occupancy': 0.7500,
 'abandonment_probability': 0.0253,
 'waiting_probability': 0.2265,
 'average_speed_of_answer': 0.1258}

Erlang B — pure loss (no waiting room)

M/M/c/c queue: blocked calls are shed immediately. Use this for SIP trunk sizing, IVR channel capacity and overflow links.

Erlang B loss system

from pyworkforce.queuing import ErlangB

erlang = ErlangB(transactions=100, aht=3, interval=30, shrinkage=0.3)

result = erlang.required_positions(max_blocking=0.02)
print(result)
{'raw_positions': 17,
 'positions': 25,
 'blocking_probability': 0.0183,
 'occupancy': 0.9375}

Running many scenarios at once

MultiErlangC, MultiErlangA and MultiErlangB sweep a parameter grid in parallel (scikit-learn style) and results_to_dataframe turns the output into a tidy table:

from pyworkforce.queuing import MultiErlangC
from pyworkforce.utils import results_to_dataframe

param_grid = {"transactions": [100], "aht": [3], "interval": [30],
              "asa": [20/60], "shrinkage": [0.3]}
multi = MultiErlangC(param_grid=param_grid, n_jobs=-1)

results = multi.required_positions({"service_level": [0.80, 0.85, 0.90],
                                    "max_occupancy": [0.85]})

df = results_to_dataframe(results, multi.required_positions_params)
print(df[["service_level", "positions", "occupancy"]].round(4).to_string(index=False))
 service_level  positions  occupancy
          0.80         20     0.7143
          0.85         20     0.7143
          0.90         22     0.6667

2 — Multi-skill staffing

When agents handle multiple contact types (e.g. English, Billing, Technical), MultiSkillStaffing finds the cheapest mix of skill profiles that meets every skill's coverage requirement. Flexible (multi-skilled) agents count towards all skills they hold.

from pyworkforce.staffing import MultiSkillStaffing

skills   = ["Billing", "Technical"]
profiles = [
    {"name": "Billing_only",   "skills": ["Billing"],              "cost": 1.0},
    {"name": "Technical_only", "skills": ["Technical"],            "cost": 1.0},
    {"name": "Flexible",       "skills": ["Billing", "Technical"], "cost": 1.5},
]
required = {"Billing": 5, "Technical": 3}

ms = MultiSkillStaffing(skills=skills, profiles=profiles, required_positions=required)
result = ms.solve()

print(f"status={result['status']}  total={result['total_agents']}  cost={result['cost']}")
for entry in result["agents_per_profile"]:
    print(f"  {entry['profile']:18s}: {entry['agents']}")
status=OPTIMAL  total=5  cost=6.5
  Billing_only      : 2
  Technical_only    : 0
  Flexible          : 3

3 flexible + 2 billing-only = cost 6.5, vs 8.0 for pure dedicated agents.


3 — Shift coverage helpers

Build the shifts_coverage arrays the schedulers expect from clock hours instead of writing 0/1 arrays by hand:

from pyworkforce.shifts import shift_coverage_from_hours

shifts_coverage = shift_coverage_from_hours({
    "Morning":   (6, 14),
    "Afternoon": (14, 22),
    "Night":     (22, 6),   # wraps past midnight
}, num_periods=24)

See also shift_coverage_from_spans, shift_coverage_from_periods, validate_shift_coverage and coverage_to_dataframe.


4 — Scheduling

Given the required resources per period, decide how many agents to place on each shift using constraint programming (OR-Tools):

from pyworkforce.scheduling import MinRequiredResources

# One row per day; each value = required agents for that hour
required_resources = [
    [9, 11, 17, 9, 7, 12, 5, 11, 8, 9, 18, 17, 8, 12, 16, 8, 7, 12, 11, 10, 13, 19, 16, 7],
    [13, 13, 12, 15, 18, 20, 13, 16, 17, 8, 13, 11, 6, 19, 11, 20, 19, 17, 10, 13, 14, 23, 16, 8],
]
shifts_coverage = {
    "Morning":   [0,0,0,0,0,1,1,1,1,1,1,1,1,0,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,1,0,0,0],
    "Night":     [1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1],
    "Mixed":     [0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0],
}

scheduler = MinRequiredResources(
    num_days=2, periods=24,
    shifts_coverage=shifts_coverage,
    required_resources=required_resources,
    max_period_concurrency=27, max_shift_concurrency=25,
)
solution = scheduler.solve()
print(solution["status"], solution["cost"])
OPTIMAL 113.0

MinAbsDifference minimises the absolute difference between required and scheduled; MinRequiredResources minimises total agents (optionally weighted by shift cost).


5 — Rostering

Assign named people to days and shifts while respecting individual rules:

from pyworkforce.rostering import MinHoursRoster

roster = MinHoursRoster(
    num_days=2,
    resources=["ana@co", "ben@co", "cara@co", "dan@co", "eve@co"],
    shifts=["Morning", "Night"],
    shifts_hours=[8, 8],
    min_working_hours=8,
    max_resting=1,
    required_resources={"Morning": [2, 2], "Night": [1, 1]},
    banned_shifts=[{"resource": "ana@co", "shift": "Night", "day": 0}],
    resources_preferences=[{"resource": "ana@co", "shift": "Morning"}],
)

solution = roster.solve()
print(solution["status"])
for a in solution["resource_shifts"][:3]:
    print(a)
OPTIMAL
{'resource': 'ana@co', 'day': 0, 'shift': 'Morning'}
{'resource': 'ana@co', 'day': 1, 'shift': 'Morning'}
{'resource': 'ben@co', 'day': 0, 'shift': 'Night'}

ana@co is assigned her preferred shift and never Night on day 0, exactly as configured.


6 — Break scheduling

Assign break start times to agent slots within each shift, ensuring that simultaneous breaks never drop coverage below the required minimum:

from pyworkforce.breaks import BreakScheduler

shifts_coverage     = {"Morning": [1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0],
                       "Afternoon": [0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1]}
scheduled_resources = {"Morning": [3], "Afternoon": [2]}
breaks = [{"name": "Lunch", "duration_periods": 2,
           "min_start_after": 2, "max_end_before": 2}]
min_coverage = [[2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1]]

scheduler = BreakScheduler(
    num_days=1, periods=16,
    shifts_coverage=shifts_coverage,
    scheduled_resources=scheduled_resources,
    breaks=breaks, min_coverage=min_coverage,
)
result = scheduler.solve()
print(result["status"])
for entry in result["break_schedule"]:
    print(entry)
OPTIMAL
{'shift': 'Morning',   'day': 0, 'slot': 0, 'break_name': 'Lunch', 'start_period': 2, 'end_period': 4}
{'shift': 'Morning',   'day': 0, 'slot': 1, 'break_name': 'Lunch', 'start_period': 4, 'end_period': 6}
{'shift': 'Morning',   'day': 0, 'slot': 2, 'break_name': 'Lunch', 'start_period': 2, 'end_period': 4}
{'shift': 'Afternoon', 'day': 0, 'slot': 0, 'break_name': 'Lunch', 'start_period': 10, 'end_period': 12}
{'shift': 'Afternoon', 'day': 0, 'slot': 1, 'break_name': 'Lunch', 'start_period': 12, 'end_period': 14}

Documentation

The documentation site includes narrative guides, a full API reference, and self-contained tutorials with real outputs:

Background reading on the underlying techniques: workforce planning and scheduling.


Contributing

Contributions are very welcome! See CONTRIBUTING.md for the local development setup, how to run the tests and linter, and the pull-request workflow.

License

pyworkforce is released under the MIT License.

Metadata

Release files for pyworkforce 0.5.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pyworkforce 0.5.4
File Size Uploaded
pyworkforce-0.5.4.tar.gz 53.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pyworkforce 0.5.4
File Interpreter ABI Platform
pyworkforce-0.5.4-py3-none-any.whl Python 3 none any Details

Total release size: 119.1 kB

Release files / pyworkforce-0.5.4.tar.gz

Download URL pyworkforce-0.5.4.tar.gz
Size 53.6 kB
Tags Source
SHA-256 checksum
How to use checksums
28b055dccd1f6f5d0b6e6cb3c7b0c1cc8af827a54914cca7c09024f556f31ada
BLAKE2b-256 checksum
How to use checksums
c0ddfdcbee5d444664669d5e11c06e331bd5da9e23a30e55f8c0fe100b6d124e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 26, 2026.

Transparency log

Release files / pyworkforce-0.5.4-py3-none-any.whl

Download URL pyworkforce-0.5.4-py3-none-any.whl
Size 65.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f88e46b56ea7d8c2eb2beae92edea19ca1841c6ffe76ab3b6cc622281b2e43dd
BLAKE2b-256 checksum
How to use checksums
e62882dda8ba8f3c34280e55bc5c89bb57b51d43e615ff8dc8e3d5f28898866d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 26, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.5.4 This release

2 release files

0.5.3

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page