Skip to main content

mod_turbotab

PyPI License

CLI-first TurboTable-style calculations for contact-center planning.

mod_turbotab keeps the historical name known by call-center planning and traffic analysts, while exposing turbotab as the primary interface for humans, scripts, and AI agents.

turbotab staffing required --sla 0.80 --service-time 20 --contacts-per-interval 25 --aht 180 --shrinkage 0.30 --json
{"calculation": "staffing.required", "inputs": {"aht": 180, "contacts_per_interval": 25.0, "interval": 600.0, "service_time": 20, "shrinkage": 0.3, "sla": 0.8}, "result": {"name": "headcount", "unit": "agents", "value": {"productive_agents": 11, "scheduled_agents": 16}}, "schema_version": "2.2"}

Why

mod_turbotab answers operational questions that show up constantly in contact-center planning:

Question Command
How many agents do I need? turbotab staffing required ...
What SLA will this staffing achieve? turbotab sla achieved ...
How long will the queue wait be? turbotab queue wait ...
How many trunks are required? turbotab telecom trunks ...
What is the Erlang B/C/A result? turbotab erlang ...

It provides Erlang B, extended Erlang B, Engset B, Erlang C, Erlang A, queue metrics, staffing metrics, contact capacity, and telephony trunk sizing with no third-party runtime dependencies.

Quick Start

Required staffing for 80% SLA in 20 seconds, with 30% shrinkage:

turbotab staffing required \
  --sla 0.80 \
  --service-time 20 \
  --contacts-per-interval 25 \
  --aht 180 \
  --shrinkage 0.30 \
  --json

--shrinkage is required: it is the fraction of paid time agents are off the phones (breaks, training, absenteeism, legally mandated rest). Pass 0 explicitly when there is none — the CLI never assumes it.

To go from per-interval seats to operators rostered across the operating day, add the optional --shifts flag (operators per seat, >= 1 — e.g. a 12-hour operation covered by 6-hour shifts needs 2.0):

turbotab staffing required \
  --sla 0.80 \
  --service-time 20 \
  --contacts-per-interval 20.0475 \
  --interval 3600 \
  --aht 480 \
  --shrinkage 0.105263 \
  --shifts 2.0 \
  --json
# result.value: {"productive_agents": 5, "scheduled_agents": 6, "rostered_agents": 12}

Omit --shifts for per-interval (intraday) sizing — the rostered_agents field only appears when the flag is passed. Note this factor is not Erlang occupancy (--max-occupancy), even though reference spreadsheets sometimes label it "occupancy".

To keep sustained agent occupancy under a cap, add the optional --max-occupancy flag (ratio in (0, 1]), available on both staffing required and staffing fractional-required. When the cap binds, productive_agents is lifted to A / max_occupancy before shrinkage — ceiled on the integer path, unrounded on the fractional path:

turbotab staffing fractional-required \
  --sla 0.80 \
  --service-time 20 \
  --contacts-per-interval 100 \
  --aht 180 \
  --shrinkage 0.30 \
  --max-occupancy 0.85 \
  --json
# result.value: {"productive_agents": 35.294117647058826, "scheduled_agents": 50.420168067226896}
# (Erlang alone would need ~34.53 productive agents; the 0.85 cap lifts the floor to 30 / 0.85.)

Achieved SLA for a fixed staffing level:

turbotab sla achieved \
  --agents 11 \
  --service-time 20 \
  --contacts-per-interval 25 \
  --aht 180 \
  --json

Average queue wait:

turbotab queue wait \
  --agents 11 \
  --contacts-per-interval 25 \
  --aht 180 \
  --json

Required trunks:

turbotab telecom trunks \
  --agents 11 \
  --contacts-per-interval 25 \
  --aht 180 \
  --json

Every command group falls back to contextual help:

turbotab
turbotab sla
turbotab staffing required --help

CLI

Agent-facing commands are intent-first:

turbotab
├── staffing
│   ├── required
│   ├── asa
│   ├── capacity
│   ├── fractional-required
│   └── fractional-capacity
├── sla
│   ├── achieved
│   └── target-time
├── queue
│   ├── wait
│   ├── size
│   └── probability
└── telecom
    └── trunks

Detailed formula/module commands are also available:

turbotab agents ...
turbotab queues ...
turbotab erlang ...
turbotab traffic ...
turbotab trunks ...

Use --json when calling from agents or automation. Invalid inputs exit non-zero and print a concise error to stderr.

Agent usage

Agents should prefer the CLI with --json instead of parsing text output or importing Python internals.

turbotab staffing required --sla 0.80 --service-time 20 --contacts-per-interval 25 --aht 180 --shrinkage 0.30 --json

JSON output is the stable agent contract:

{
  "schema_version": "2.2",
  "calculation": "staffing.required",
  "inputs": {
    "aht": 180,
    "contacts_per_interval": 25.0,
    "interval": 600.0,
    "service_time": 20,
    "shrinkage": 0.3,
    "sla": 0.8
  },
  "result": {
    "name": "headcount",
    "unit": "agents",
    "value": {
      "productive_agents": 11,
      "scheduled_agents": 16
    }
  }
}

staffing required and staffing fractional-required emit the headcount chain under schema_version 2.2 (2.1 plus the optional rostered_agents field, present only when --shifts is passed); commands whose inputs or result fields were renamed by the contacts terminology sweep use 1.1; unaffected raw-formula commands (erlang, traffic intensity, trunks number) keep the original 1.0 payloads.

The bundled skill lives at skills/mod-turbotab/SKILL.md. It includes command recipes, unit rules, and agent guardrails.

Units and assumptions

This project uses interval-based planning buckets.

Every function or CLI command that accepts contact volume uses contacts_per_interval, not contacts per hour by default.

Parameter Meaning
contacts_per_interval / --contacts-per-interval Arrivals in the planning bucket
interval / --interval Planning bucket in seconds
Default interval 600 seconds, or 10 minutes
aht / --aht Average handle time in seconds
service_time / --service-time Target answer time in seconds
sla / --sla Ratio, for example 0.80 for 80%

For hourly semantics, pass --interval 3600:

turbotab staffing required --sla 0.80 --service-time 20 --contacts-per-interval 150 --aht 180 --interval 3600 --shrinkage 0 --json

Traffic intensity is computed as:

A = \frac{\lambda \cdot h}{I}

where A is offered traffic in erlangs, lambda is arrivals per interval, h is AHT in seconds, and I is interval length in seconds.

Worked example

With the default 10-minute bucket:

Input Value
Contacts 25 per 10 minutes
AHT 180 seconds
Target SLA 0.80
Target answer time 20 seconds
Shrinkage 0.30

CLI:

turbotab staffing required --sla 0.80 --service-time 20 --contacts-per-interval 25 --aht 180 --shrinkage 0.30 --json
turbotab sla achieved --agents 11 --service-time 20 --contacts-per-interval 25 --aht 180 --json
turbotab queue wait --agents 11 --contacts-per-interval 25 --aht 180 --json
turbotab telecom trunks --agents 11 --contacts-per-interval 25 --aht 180 --json

The queue metrics (sla achieved, queue wait, telecom trunks) take the productive agents — shrinkage covers who is off the phones, not queue behavior.

Expected headline results:

Metric Result
Productive agents (on phones) 11
Scheduled agents (after 30% shrinkage) 16
Achieved SLA 0.880836
Average queue wait 51 seconds
Required trunks 18

Python API equivalent:

from mod_turbotab.agents.capacity import agents_required
from mod_turbotab.agents.shrinkage import scheduled_agents

productive = agents_required(0.80, 20, 25, 180)
scheduled = scheduled_agents(productive, 0.30)

print(productive, scheduled)  # 11 16
Mathematical model

Notation:

Symbol Meaning
N Agents, servers, or trunks depending on context
lambda Arrival volume per configured interval
h Average handle time in seconds
I Planning interval in seconds
mu Service completions per interval per server
A Offered traffic in erlangs
rho Utilization
B(N, A) Erlang B blocking probability
C(N, A) Erlang C queueing probability
rho_max Optional occupancy cap (max_occupancy)
s Multi-skill sharing factor (sharing_factor)
S Shrinkage factor (shrinkage)
M Shift/seat multiplier (shifts)

Core conversions:

\mu = \frac{I}{h}
A = \frac{\lambda}{\mu} = \frac{\lambda h}{I}
\rho = \frac{A}{N}

Erlang B recurrence:

B_0 = 1
B_n = \frac{A B_{n-1}}{n + A B_{n-1}}

Erlang C:

C(N, A) = \frac{B(N, A)}{\left(\frac{A}{N}\right) B(N, A) + \left(1 - \frac{A}{N}\right)}

Queue wait:

W_q = \frac{1}{N \mu (1 - \rho)}

SLA:

\mathrm{SLA}(t) = 1 - C(N, A)\exp\left(-\frac{N - A}{h}t\right)

ASA:

\mathrm{ASA} = \frac{C(N, A)}{N \mu (1 - \rho)}

Erlang A extends Erlang C with abandonment through average patience. When patience=None, pure Erlang C is used.

Occupancy cap, when max_occupancy is set on agents_required (or --max-occupancy on the CLI):

N = \max\left(N_{\mathrm{Erlang}},\ \left\lceil \frac{A}{\rho_{\max}} \right\rceil\right)

With max_occupancy=None the cap is skipped and the Erlang result is returned unchanged.

The fractional counterpart (max_occupancy on fractional_agents, or --max-occupancy on staffing fractional-required / agents fractional-required) applies the same floor without rounding, leaving all rounding to the caller:

N^{\mathrm{frac}} = \max\left(N_{\mathrm{Erlang}}^{\mathrm{frac}},\ \frac{A}{\rho_{\max}}\right)

Multi-skill dimensioning (agents_required_multi, Option A): each skill group k is first sized as an independent Erlang C queue, giving N_k^{C}. Skills served by at least one cross-skilled pool then receive the sharing factor s, floored so per-skill utilization stays strictly below 100%:

N_k = \max\left(\left\lceil A_k \right\rceil + 1,\ \left\lceil s \cdot N_k^{C} \right\rceil\right)

Skills served only by dedicated pools keep N_k = N_k^{C}, reproducing the single-skill result.

Shrinkage (scheduled_agents, agents_required_with_shrinkage, Option A): the Erlang result counts agents on the phones; scheduling must also cover breaks, training, meetings, absenteeism and downtime. The standard workforce-management correction is applied post-calculation, leaving the core Erlang math unchanged:

N_{\mathrm{scheduled}} = \left\lceil \frac{N_{\mathrm{phones}}}{1 - S} \right\rceil

S must be in [0, 1) and can be composed from individual components with shrinkage_factor (components are additive slices of paid time off the phones). With S = 0 the result is the unchanged Erlang headcount.

The fractional counterpart (scheduled_fractional_agents) applies the same correction without rounding, for chains built on fractional_agents:

N_{\mathrm{scheduled}}^{\mathrm{frac}} = \frac{N_{\mathrm{phones}}^{\mathrm{frac}}}{1 - S}

On the CLI, S is the mandatory --shrinkage flag of staffing required and staffing fractional-required; both emit the full productive_agents/scheduled_agents chain.

Shift/seat multiplier (rostered_agents): scheduled_agents counts seats per interval; covering the operating day takes more than one operator per seat when shifts are shorter than the day. The multiplier M (operators per seat, M >= 1, fractional allowed) is applied on top of the shrinkage chain, rounding step-wise — seats are made whole before multiplying, which never understaffs relative to rounding once at the end:

N_{\mathrm{rostered}} = \left\lceil N_{\mathrm{scheduled}} \cdot M \right\rceil

The fractional counterpart (rostered_fractional_agents) applies the same multiplier without rounding:

N_{\mathrm{rostered}}^{\mathrm{frac}} = N_{\mathrm{scheduled}}^{\mathrm{frac}} \cdot M

On the CLI, M is the optional --shifts flag of staffing required and staffing fractional-required; when passed, the chain extends to productive_agents/scheduled_agents/rostered_agents. M is not Erlang occupancy (rho_max), even though reference spreadsheets sometimes overload that name for it.

API reference

The CLI is the primary interface, but the Python API remains available.

Module Public functions
calculations.erlang erlang_b, erlang_b_ext, engset_b, erlang_c, erlang_a
calculations.traffic traffic, looping_traffic
calculations.multi_skill agents_required_multi
agents.capacity agents_required, asa, agents_asa, nb_agents, contact_capacity, fractional_agents, fractional_contact_capacity, occupancy, is_within_occupancy
agents.shrinkage scheduled_agents, scheduled_fractional_agents, shrinkage_factor, agents_required_with_shrinkage
agents.roster rostered_agents, rostered_fractional_agents
queues.queues queued, queue_size, queue_time, service_time, sla_metric
trunks.trunks number_trunks, trunks_required
utils min_max, int_ceiling, secs

Import example:

from mod_turbotab.agents.capacity import agents_required

agents = agents_required(
    sla=0.80,
    service_time=20,
    contacts_per_interval=25,
    aht=180,
)

Multi-skill example — dedicated pools plus a cross-skilled pool sharing billing and tech:

from mod_turbotab.calculations.multi_skill import agents_required_multi

result = agents_required_multi(
    skill_groups=[
        {"name": "billing", "contacts_per_interval": 25, "aht": 180},
        {"name": "tech",    "contacts_per_interval": 20, "aht": 240},
    ],
    agent_pools=[
        {"skills": ["billing"],         "count": 8},
        {"skills": ["tech"],            "count": 9},
        {"skills": ["billing", "tech"], "count": 6},
    ],
    sla=0.80,
    service_time=20,
)

result["totals"]  # {"naive_total_hc": 22, "adjusted_total_hc": 20, "savings_hc": 2, ...}

Shrinkage example — turning "agents on phones" into "agents to schedule":

from mod_turbotab.agents.shrinkage import (
    agents_required_with_shrinkage,
    scheduled_agents,
    shrinkage_factor,
)

factor = shrinkage_factor(breaks=0.07, training=0.04, absenteeism=0.08)  # 0.19

# One call: Erlang C + shrinkage on top
agents_required_with_shrinkage(
    sla=0.80,
    service_time=20,
    contacts_per_interval=25,
    aht=180,
    shrinkage=factor,
)  # 14

# Or compose manually from an existing headcount
scheduled_agents(11, factor)  # 14

Shift/seat multiplier example — turning scheduled seats into rostered operators:

from mod_turbotab.agents.roster import rostered_agents

# 6 scheduled seats, 12h operation covered by 6h shifts (2 operators per seat)
rostered_agents(6, 2.0)  # 12
Exceptions

Project-specific exceptions live in exceptions.py:

Exception Meaning
InputValidationError Invalid argument values
CalculationError Calculation failed or search could not converge

Example:

from mod_turbotab.exceptions import CalculationError, InputValidationError

try:
    ...
except InputValidationError:
    ...
except CalculationError:
    ...

Limitations

  • number_trunks() uses a fixed blocking threshold of 0.001.
  • Some zero-value edge cases still return wrapped calculation errors instead of purpose-built validation messages.
  • Intraday simulation is tracked as future work — see issues labeled roadmap.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

turbotab-0.8.0.tar.gz (78.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

turbotab-0.8.0-py3-none-any.whl (58.2 kB view details)

Uploaded Python 3

File details

Details for the file turbotab-0.8.0.tar.gz.

File metadata

  • Download URL: turbotab-0.8.0.tar.gz
  • Upload date:
  • Size: 78.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for turbotab-0.8.0.tar.gz
Algorithm Hash digest
SHA256 f2da7eafd5809dd5c48048f94dafdadcadddbb852fedb7d262df5f0295d994ef
MD5 c8c5bede4d0b45fd816671fd8a6a89d8
BLAKE2b-256 9c8c61164b09fd6cc030890d764f2401b33b00b7969e18b1edf6ed687e98f6dc

See more details on using hashes here.

Provenance

The following attestation bundles were made for turbotab-0.8.0.tar.gz:

Publisher: publish.yml on gstvbatista/mod_turbotab

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file turbotab-0.8.0-py3-none-any.whl.

File metadata

  • Download URL: turbotab-0.8.0-py3-none-any.whl
  • Upload date:
  • Size: 58.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for turbotab-0.8.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ff778a2e38928a360fc1318d8c13a4a51c52deb8239068435be3d4900706f0f3
MD5 c89837b99d558a7527f3009df4ce02c6
BLAKE2b-256 d8925e9b2f7bbdb77cb3cdd022d6fe8c9e0763b1e13cb4fd4131ddb4eab2a74a

See more details on using hashes here.

Provenance

The following attestation bundles were made for turbotab-0.8.0-py3-none-any.whl:

Publisher: publish.yml on gstvbatista/mod_turbotab

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page