IT Support & Help Desk Fundamentals Module 12 – Service desk practice
Tickets, priority and SLAs that hold up
Walk a ticket through its states, set priority from a 3x3 matrix, compute an SLA deadline across time zones and holidays, and spot metrics that can be gamed.
What you will learn
- Move a ticket through its states correctly, from new to closed, reopening a related ticket instead of a duplicate
- Set priority from impact and urgency with a published matrix
- Calculate an SLA deadline in business hours across time zones and holidays
- Read service-desk metrics and show how they can be gamed
Before you start
What you’ll learn · Before you start
What you will learn
- Move a ticket through its states correctly, from new to closed, reopening a related ticket instead of a duplicate
- Set priority from impact and urgency with a published matrix
- Calculate an SLA deadline in business hours across time zones and holidays
- Read service-desk metrics and show how they can be gamed
Before you start
On this page
A help desk works 09:00 to 18:00 IST, Monday to Friday. A ticket: The record of one support request: who it is for, the problem, every action taken with its time, and how it was resolved. The service desk works and hands on tickets, and closes one when the problem is fixed. More in “Ticket notes and handoffs people can use” with a 60-minute resolution target lands at 17:30 on a Thursday, and the Friday is a published holiday. Added on a wall clock, the 60 minutes end at 18:30 that evening, so the ticket breaches while nobody is at the desk; counted in business minutes against the holiday calendar, the same promise ends on Monday at 09:30.
Tickets, priority and SLAs hold up when each fault keeps one ticket with its whole history, priority is read off a matrix the organisation has published, the SLA clock counts only the desk’s open minutes, a customer’s time is converted to the desk’s clock before any minute is added, and the desk’s metrics are read together so no single number can be gamed. Each ticket is written as the ticket notes and handoffs lesson showed, with the exact error text and an action log: The time-ordered list of what was done on a ticket: one action per line, each with its result and a timestamp that carries a UTC offset, so any shift can follow what happened and when. More in “Ticket notes and handoffs people can use” whose entries carry a UTC offset.
A ticket moves through six states, and a returning fault reopens it
A ticket is not a flat note; it moves. Its six states are new, assigned, in progress, pending, resolved and closed. New means the request has arrived and nobody owns it; assigned means a triager or a routing rule has given it an owner; in progress means the owner is working on it; pending means the owner is waiting on a reply, usually from the user or a vendor; resolved means the fix is in and the user has been told; and closed means an idle window has passed with no objection. The pending state: The state a ticket moves to while the owner is waiting on a reply, usually from the user or a vendor. The service-level agreement clock pauses while a ticket is pending only if the agreement itself names that rule. pauses the service-level agreement: A written contract between the service desk and its users or customers, naming the targets the desk commits to for each priority (response time, resolution time) and the business hours and holidays those targets are measured against. Its short name is SLA. clock only when the agreement itself says so, which is why the words of the agreement matter as much as the states.
Step through one ticket from the first call to the closed window, and watch the last step, where the same fault comes back after the ticket has closed:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- A new request lands. A user calls, writes or opens a portal form, and the record is created with the description and the start time. Nobody owns it yet, so the state is new.
- An owner picks it up. A triager or a routing rule gives the ticket an owner, and its state becomes assigned. The clock the agreement sets started at the request time, not now.
- The work starts. The owner begins the diagnosis and logs each action. The state is in progress. The response-target clock, if one applies, stops when the first substantial reply reaches the user.
- Waiting on a reply. The owner needs a reply from the user or a vendor to go further, so the state moves to pending. The resolution clock pauses only if the agreement says a pending state pauses it, so read before you promise.
- Fix landed, window closes. The reply arrives, the fix is delivered, and the state becomes resolved. After an idle window the user has to object in, the ticket closes with the resolution note that explains what the fix was.
- The fault returns, reopen. The user hits the same error the next morning. Reopen the related ticket rather than open a fresh one, so the history travels with the work and metrics stay honest. The state goes back to in progress.
The arrow back from closed is the one most new analysts miss. When the same fault returns, reopen the related ticket rather than open a fresh one. The history travels with the work, the next technician reads what was already tried, and the clock keeps the minutes already spent. A duplicate loses the history, counts the second contact as first-contact resolution and makes the fault look rarer than it is. A fresh ticket is right for a different fault on the same device, and a symptom alone does not always tell the two apart, so compare the exact error text with the earlier ticket before you choose.
A fresh ticket for a returning fault
Wrong: a VIP marks a ticket closed on their own, hits the same error ten minutes later, and the analyst opens a new one that resolves at first contact. Right: reopen the related ticket, keep the time already on the clock, and record the second contact in the same thread. The second route is slower today and honest in every metric.
Checkpoint
Check: States and the reopen arrow
Priority is impact and urgency, read off a published matrix
Priority answers one question: which ticket is worked next. It is not how the caller feels, and it is not first come, first served. The two inputs are impact: In service-desk work: how many people a fault affects and how critical the service it stops is. One axis of the priority matrix: low, medium or high., how many people a fault affects or how critical the service is, and urgency: In service-desk work: how fast the harm from a fault grows if it is left in place. The other axis of the priority matrix: low (no deadline), medium (today) or high (minutes to harm)., how fast the harm grows if the fault is left in place. Each is read on three steps (low, medium, high), so the priority matrix: A 3x3 table that reads impact against urgency and prints the priority band (such as P1 to P4) for each cell. Every organisation publishes its own matrix in its service-level agreement. has nine cells and four priority bands. Each organisation sets its own matrix and the targets of each band in its SLA; CompTIA’s A+ Core 2 objectives name severity and escalation levels among the parts of a ticketing system, and list service-level agreements among the documents a support technician works with. The table below is the sample MySmartCoPilot uses in this lesson; at work, read your own organisation’s matrix.
| Urgency (rows) by impact (columns) | Low: one user | Medium: a team | High: a site or a critical service |
|---|---|---|---|
| Low: no deadline | P4 | P3 | P2 |
| Medium: today | P3 | P2 | P2 |
| High: minutes to harm | P2 | P2 | P1 |
Find a ticket’s row and its column, and the cell is its priority. Two tickets show how. A VIP’s forgotten password is low impact (one user) and medium urgency (mail still works on their phone, so the harm grows through the day, not by the minute), so it is P3. The stopped payroll server is high impact (a critical service for the whole organisation) and high urgency (the payroll team cannot work, and every minute comes out of the time left before tomorrow’s pay run), so it is P1. P1 goes first even if the VIP called first, and the VIP gets a short message that says what you are doing and when the next update will come. When a priority does have to change, the change and its reason are written in the ticket.
Key idea
Priority comes from two readings, impact and urgency, looked up on a matrix the organisation has already written. A change of priority is a decision with its reason in the ticket, not a judgement about the caller.
Checkpoint
Check: Priority from a published matrix
An SLA clock counts only business minutes, and holidays count none
A service-level agreement commits the desk to two clocks for each priority. The response target: How soon, in business hours, a service-level agreement commits a human reply to the user after a ticket of a given priority is raised. The response clock stops at the first substantial reply. is how soon the user hears from a person; the resolution target: How soon, in business hours, a service-level agreement commits the fault to be fixed for a ticket of a given priority. Measured against the business-hours calendar and the holiday calendar for the office. is how soon the fault is fixed. Both are measured in business hours, against the published business-hours calendar: The published daily working hours of a service desk, such as 09:00 to 18:00 Monday to Friday. Service-level agreement clocks count minutes only inside these windows, so a wall-clock promise is replaced with a business-hours one. (here 09:00 to 18:00, Monday to Friday) and the office’s holiday calendar: The list of dates on which an office is closed and no business minutes elapse, such as a country's public holidays or an organisation's shutdowns. The service-level agreement reads it beside the business hours.. A promise in wall-clock hours runs on through nights, weekends and holidays, when nobody is working the ticket; a promise in business hours counts only the minutes the desk is open. The MySmartCoPilot public holidays tool lists India’s Central Government holidays, US federal holidays and UK bank holidays when you need a real calendar.
Take case 1 of the program below. The request lands at 17:30+05:30 on a Thursday and the P2 resolution target is 60 business minutes. The Friday is a holiday on the desk’s calendar, so the clock has to carry 30 of its 60 minutes across the holiday and the weekend to Monday. Follow it phase by phase:
- 17:30+05:30
Request lands at 17:30 on a Thursday start
The user reports the fault 30 minutes before close. The desk promises a 60-business-minute resolution, so the agreement clock begins at this moment on the IST business calendar.
- 18:00+05:30
Close takes the first 30 minutes in progress
The desk closes at 18:00. Thirty of the sixty minutes have been spent inside Thursday business hours, so 30 minutes are left to spend on a later working day.
- Friday is off
The published holiday counts nothing paused
The Friday is a sample holiday on this desk, and the holiday calendar says it is not a working day. Zero business minutes elapse on it, and the clock does not advance.
- Weekend is off
Saturday and Sunday count nothing paused
Saturday and Sunday are not working days, so no business minute is spent on either of them. The clock stays at 30 minutes to go into Monday.
- 09:00+05:30
Monday open resumes the clock in progress
The desk opens on Monday at 09:00 and the clock resumes with 30 business minutes still to spend. Every minute from here counts.
- 09:30+05:30
Deadline lands on Monday 09:30 deadline
Thirty minutes after open, the clock has spent the second half of the hour. The resolution deadline is Monday 09:30+05:30, four calendar days after the request but 60 business minutes after it.
The timeline has 6 events, from 17:30+05:30 to 09:30+05:30.
The program does the same sum in Python, so the timeline and its output agree. Cases 2, 3 and 4 add three awkward starts: a Saturday, a time written in UTC and a time before the desk opens.
"""Add business minutes across holidays and time zones for one service desk.
Every moment below is invented. The desk runs 09:00-18:00 India Standard Time
(IST), Monday to Friday, and a sample holiday falls on Friday 2026-10-02. The
IANA tz database file for Asia/Kolkata gives IST a constant +05:30 offset, so
Pyodide (which has no zoneinfo) can model it with datetime.timezone and the
times carry a UTC offset the way RFC 3339 asks.
"""
from datetime import date, datetime, time, timedelta, timezone
IST = timezone(timedelta(hours=5, minutes=30)) # Asia/Kolkata, +05:30
UTC = timezone.utc
DEFAULT_HOURS = (time(9, 0), time(18, 0)) # 09:00 to 18:00 local
def add_business_minutes(start, minutes, holidays=(), hours=DEFAULT_HOURS):
"""Return the moment that is `minutes` business minutes after `start`.
- `start` is an aware datetime. If it is not already in IST, it is read as
an IST clock first, so the business hours belong to the desk.
- `hours` is an (open, close) pair of local times; the default is 09:00
to 18:00, nine working hours a day.
- Days off: Saturday, Sunday, and any date in `holidays`.
- If the start sits before opening, the clock starts at the day's open.
If at or after close, or on a weekend or holiday, the clock rolls to
the next working day's open before any minutes are added.
"""
if start.tzinfo is None:
raise ValueError("start must be timezone-aware")
cursor = start.astimezone(IST)
open_t, close_t = hours
remaining = timedelta(minutes=minutes)
off_days = set(holidays)
while True:
day = cursor.date()
day_open = datetime.combine(day, open_t, tzinfo=IST)
day_close = datetime.combine(day, close_t, tzinfo=IST)
if cursor.weekday() >= 5 or day in off_days or cursor >= day_close:
cursor = datetime.combine(day + timedelta(days=1), open_t, tzinfo=IST)
continue
if cursor < day_open:
cursor = day_open
left_today = day_close - cursor
if remaining <= left_today:
return cursor + remaining
remaining -= left_today
cursor = datetime.combine(day + timedelta(days=1), open_t, tzinfo=IST)
def iso(when):
"""ISO 8601 to the minute, with the colon offset: 2026-10-05T09:30+05:30."""
return when.isoformat(timespec="minutes")
WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
def day_name(when):
return WEEKDAYS[when.weekday()]
# Case 1: Thursday evening, Friday is a published holiday, +60 business minutes.
holiday = {date(2026, 10, 2)}
case1_start = datetime(2026, 10, 1, 17, 30, tzinfo=IST)
case1_end = add_business_minutes(case1_start, 60, holidays=holiday)
print("Case 1 - Thursday 17:30 with Friday 2026-10-02 as a holiday, +60 min:")
print(f" start: {iso(case1_start)} ({day_name(case1_start)})")
print(f" deadline: {iso(case1_end)} ({day_name(case1_end)})")
print()
# Case 2: a start on a Saturday rolls forward to Monday's open.
case2_start = datetime(2026, 10, 10, 10, 0, tzinfo=IST)
case2_end = add_business_minutes(case2_start, 60)
print("Case 2 - Saturday morning, +60 min (no extra holiday):")
print(f" start: {iso(case2_start)} ({day_name(case2_start)})")
print(f" deadline: {iso(case2_end)} ({day_name(case2_end)})")
print()
# Case 3: a UTC input is converted to the IST business clock first.
case3_start = datetime(2026, 10, 7, 10, 0, tzinfo=UTC)
case3_end = add_business_minutes(case3_start, 45)
print("Case 3 - a UTC input, read on the IST clock first, +45 min:")
print(f" start (UTC): {iso(case3_start)} ({day_name(case3_start)})")
print(f" start (IST): {iso(case3_start.astimezone(IST))}")
print(f" deadline: {iso(case3_end)} ({iso(case3_end.astimezone(UTC))} UTC)")
print()
# Case 4: a start before opening on the same day uses the day's open.
case4_start = datetime(2026, 10, 12, 7, 15, tzinfo=IST)
case4_end = add_business_minutes(case4_start, 90)
print("Case 4 - Monday 07:15 (before the desk opens), +90 min:")
print(f" start: {iso(case4_start)} ({day_name(case4_start)})")
print(f" deadline: {iso(case4_end)} ({day_name(case4_end)})") Output
Case 1 - Thursday 17:30 with Friday 2026-10-02 as a holiday, +60 min:
start: 2026-10-01T17:30+05:30 (Thursday)
deadline: 2026-10-05T09:30+05:30 (Monday)
Case 2 - Saturday morning, +60 min (no extra holiday):
start: 2026-10-10T10:00+05:30 (Saturday)
deadline: 2026-10-12T10:00+05:30 (Monday)
Case 3 - a UTC input, read on the IST clock first, +45 min:
start (UTC): 2026-10-07T10:00+00:00 (Wednesday)
start (IST): 2026-10-07T15:30+05:30
deadline: 2026-10-07T16:15+05:30 (2026-10-07T10:45+00:00 UTC)
Case 4 - Monday 07:15 (before the desk opens), +90 min:
start: 2026-10-12T07:15+05:30 (Monday)
deadline: 2026-10-12T10:30+05:30 (Monday)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sla.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Case 2 starts on a Saturday, so nothing counts until Monday’s 09:00 open and the 60 minutes end at 10:00. Case 3’s 10:00 UTC is 15:30 on the desk’s clock, and 45 minutes later is 16:15+05:30, which the program also prints as 10:45 UTC. Case 4 starts at 07:15, before opening, so its 90 minutes run from 09:00 and end at 10:30, 195 minutes after the start on the wall clock.
The wall-clock promise
Wrong: “resolve in 4 hours”, carried straight to the ticket. Right: “resolve in 4 business hours on the published 09:00 to 18:00 schedule, with every holiday on the office calendar counting zero minutes”. The first slips every evening; the second lands on a working minute.
Checkpoint
Check: SLA clocks in business hours
Convert a customer’s time to the desk’s clock before adding minutes
India Standard Time is a fixed offset of +05:30: the IANA tz database’s asia file gives Asia/Kolkata that offset with no daylight saving since 1945. Many customers do not live on a fixed offset. The United Kingdom moves between Greenwich Mean Time (+00:00) and British Summer Time (+01:00) on the last Sundays of March and October, and most of the United States moves on dates of its own in March and November, as the tz database’s europe and northamerica files record. A deadline promised to such a customer needs a time-zone library that knows those rules, not an offset typed in once, which is right for only part of the year. The timestamp converter and the timezone converter do the same conversion by hand when you read a ticket from another clock.
Follow one UK customer’s time through the steps below; the dashed arrow back is the ticket whose time has no offset:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Receive the time as written. A UK customer reports a fault at Monday 20:30+01:00. Keep the time exactly as the customer wrote it, offset included, because the offset says which moment they mean.
- Pin the offset, or ask. The offset is +01:00, and the UK keeps British Summer Time (+01:00) from the last Sunday of March to the last Sunday of October, so it fits the date. A time with no offset and no city names no single moment: ask the user, and start again when they reply.
- Convert to the desk's clock. The desk works on India Standard Time, which the IANA tz database gives Asia/Kolkata as +05:30 all year. 20:30+01:00 is 19:30 UTC, which is 01:00+05:30 on Tuesday on the desk clock.
- Count the business minutes. 01:00 is before the desk opens, so no minute counts until 09:00. A four-business-hour promise then ends at 13:00+05:30 on Tuesday, well inside the 09:00 to 18:00 day.
- Write the deadline twice. Give the customer the deadline on their own clock: 13:00+05:30 is 07:30 UTC, which is 08:30+01:00 in the UK. Keep both forms in the ticket, so whoever picks it up next reads the same moment.
The order matters: pin the offset, convert to the desk’s clock, add the SLA minutes across business hours and holidays, then write the deadline on the customer’s own clock as well, so they can plan. A time with no offset is a problem the desk cannot solve on its own. “I called at 20:30” names no single moment: 20:30 in Kolkata, in London and in New York are three instants, hours apart. Ask for the city or the offset before you promise a deadline. That is also why the ticket notes lesson writes every action-log entry as an ISO 8601 timestamp: A date and time written in the international ISO 8601 order, from the largest unit to the smallest, with a UTC offset after the time, as in a time written 14:05+05:30, or a trailing Z for UTC. More in “Ticket notes and handoffs people can use” with its offset; RFC 3339, the internet profile of ISO 8601, makes the offset, or a Z for UTC, part of every timestamp.
Key idea
Convert the time to the desk’s clock before you add minutes to it. A customer’s time without an offset is a question, not a start.
Checkpoint
Check: Offsets and daylight saving
Each desk metric can be gamed, so read them together
Service-desk metrics are easy to compute and easy to game. Four that a desk is judged on: first-contact resolution: The share of closed tickets that were resolved during the first contact with the user, with no later work needed. Written FCR. Easy to flatter by splitting one ticket into two or by closing early. (the share of closed tickets resolved during the first contact), mean time to resolve: The average time between a ticket's creation and its resolution, taken over the closed tickets in a window. Written MTTR. Falls alongside a rising reopen rate when tickets are being closed too soon. (how long a ticket takes on average, from creation to resolution), backlog age: The average age of the tickets that are still open at the moment the report is run. Measured in hours or days; it moves only when tickets are opened, closed or stay open longer, not when a closed ticket is split. (how old the open tickets are on average) and the reopen rate: The share of closed tickets that had to be reopened within a window, usually a few days. A rising reopen rate beside a falling mean time to resolve is the signature of a desk closing tickets too early. (the share of closed tickets that had to be reopened). A fifth, satisfaction, needs the user and is outside this short example.
Each one has a trick that lifts the number without a better outcome for the user. The example reads an invented week of 15 tickets and prints the four numbers. Then it splits T-02, a ticket that was not resolved at first contact, into two tickets that are both marked as resolved at first contact, and prints them again:
"""Read a week of invented tickets and show four service-desk metrics on them,
then show how splitting one ticket into two inflates first-contact resolution
without any change for the user.
All ticket data is invented. The fixed "now" the report uses is
2026-10-19T18:00+05:30, so backlog age is deterministic. The clock is a fixed
+05:30 offset (Asia/Kolkata, from the IANA tz database) because Pyodide, the
browser Python runtime, has no zoneinfo.
"""
from datetime import datetime, timedelta, timezone
IST = timezone(timedelta(hours=5, minutes=30)) # +05:30
NOW = datetime(2026, 10, 19, 18, 0, tzinfo=IST) # the fixed "now"
# One row per ticket: (id, created, resolved | None, resolved on first contact, reopens).
TICKETS = [
("T-01", datetime(2026, 10, 12, 9, 30, tzinfo=IST), datetime(2026, 10, 12, 10, 5, tzinfo=IST), True, 0),
("T-02", datetime(2026, 10, 12, 10, 0, tzinfo=IST), datetime(2026, 10, 13, 12, 45, tzinfo=IST), False, 0),
("T-03", datetime(2026, 10, 12, 11, 15, tzinfo=IST), datetime(2026, 10, 12, 11, 40, tzinfo=IST), True, 0),
("T-04", datetime(2026, 10, 13, 9, 10, tzinfo=IST), datetime(2026, 10, 13, 10, 0, tzinfo=IST), True, 1),
("T-05", datetime(2026, 10, 13, 11, 0, tzinfo=IST), datetime(2026, 10, 14, 15, 20, tzinfo=IST), False, 0),
("T-06", datetime(2026, 10, 14, 10, 45, tzinfo=IST), datetime(2026, 10, 14, 11, 10, tzinfo=IST), True, 0),
("T-07", datetime(2026, 10, 14, 14, 0, tzinfo=IST), datetime(2026, 10, 15, 9, 45, tzinfo=IST), False, 0),
("T-08", datetime(2026, 10, 15, 9, 20, tzinfo=IST), datetime(2026, 10, 15, 10, 0, tzinfo=IST), True, 0),
("T-09", datetime(2026, 10, 15, 11, 30, tzinfo=IST), datetime(2026, 10, 16, 10, 10, tzinfo=IST), False, 1),
("T-10", datetime(2026, 10, 15, 15, 10, tzinfo=IST), datetime(2026, 10, 15, 16, 0, tzinfo=IST), True, 0),
("T-11", datetime(2026, 10, 16, 9, 0, tzinfo=IST), datetime(2026, 10, 16, 11, 30, tzinfo=IST), True, 0),
("T-12", datetime(2026, 10, 16, 14, 20, tzinfo=IST), None, False, 0),
("T-13", datetime(2026, 10, 16, 16, 0, tzinfo=IST), None, False, 0),
("T-14", datetime(2026, 10, 19, 9, 10, tzinfo=IST), datetime(2026, 10, 19, 9, 55, tzinfo=IST), True, 0),
("T-15", datetime(2026, 10, 19, 11, 0, tzinfo=IST), None, False, 0),
]
def closed(tickets):
return [t for t in tickets if t[2] is not None]
def still_open(tickets):
return [t for t in tickets if t[2] is None]
def first_contact_resolution(tickets):
done = closed(tickets)
if not done:
return 0.0
hits = sum(1 for t in done if t[3])
return 100 * hits / len(done)
def mean_time_to_resolve(tickets):
done = closed(tickets)
if not done:
return timedelta(0)
total = sum((t[2] - t[1] for t in done), timedelta())
return total / len(done)
def backlog_age(tickets, now):
opened = still_open(tickets)
if not opened:
return timedelta(0)
total = sum((now - t[1] for t in opened), timedelta())
return total / len(opened)
def reopen_rate(tickets):
done = closed(tickets)
if not done:
return 0.0
hits = sum(1 for t in done if t[4] > 0)
return 100 * hits / len(done)
def hhmm(td):
"""Hours and minutes form of a timedelta: 8:42."""
total = int(td.total_seconds())
hours, rest = divmod(total, 3600)
minutes = rest // 60
return f"{hours}:{minutes:02d}"
def report(label, tickets):
print(f"{label}:")
print(f" tickets: {len(tickets)} ({len(closed(tickets))} closed, {len(still_open(tickets))} open)")
print(f" FCR: {first_contact_resolution(tickets):.1f} %")
print(f" MTTR: {hhmm(mean_time_to_resolve(tickets))} (hours:minutes)")
print(f" reopen rate: {reopen_rate(tickets):.1f} %")
print(f" backlog age: {hhmm(backlog_age(tickets, NOW))} (hours:minutes)")
print()
report("Service-desk week, as recorded", TICKETS)
# Game: split T-02 into T-02a and T-02b, both "resolved on first contact".
# Nothing changes for the user; the second ticket wraps up with the same
# resolution a day later. FCR, MTTR and the reopen rate all move; only the
# ticket count (16 tickets for the same 15 problems) gives the split away.
original = next(t for t in TICKETS if t[0] == "T-02")
gamed = []
for t in TICKETS:
if t[0] == "T-02":
gamed.append(("T-02a", original[1], original[1] + timedelta(minutes=20), True, 0))
gamed.append(("T-02b", original[1] + timedelta(minutes=25), original[2], True, 0))
else:
gamed.append(t)
report("Same week, with T-02 split into T-02a and T-02b (both 'first contact')", gamed)
base_fcr = first_contact_resolution(TICKETS)
gamed_fcr = first_contact_resolution(gamed)
base_mttr = mean_time_to_resolve(TICKETS)
gamed_mttr = mean_time_to_resolve(gamed)
delta_fcr = gamed_fcr - base_fcr
print(f"FCR moved from {base_fcr:.1f} % to {gamed_fcr:.1f} % (up {delta_fcr:.1f} points).")
print(f"MTTR moved from {hhmm(base_mttr)} to {hhmm(gamed_mttr)} (hours:minutes).")
print("The user got nothing different; three of the four numbers now look better.") Output
Service-desk week, as recorded:
tickets: 15 (12 closed, 3 open)
FCR: 66.7 %
MTTR: 8:42 (hours:minutes)
reopen rate: 16.7 %
backlog age: 52:13 (hours:minutes)
Same week, with T-02 split into T-02a and T-02b (both 'first contact'):
tickets: 16 (13 closed, 3 open)
FCR: 76.9 %
MTTR: 8:01 (hours:minutes)
reopen rate: 15.4 %
backlog age: 52:13 (hours:minutes)
FCR moved from 66.7 % to 76.9 % (up 10.3 points).
MTTR moved from 8:42 to 8:01 (hours:minutes).
The user got nothing different; three of the four numbers now look better.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 metrics.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
First-contact resolution rose from 66.7 to 76.9 per cent and mean time to resolve fell from 8:42 to 8:01 (hours and minutes). The reopen rate slipped from 16.7 to 15.4 per cent too, because the split added one closed ticket and no reopen. Backlog age stayed at 52:13, because no open ticket moved. Three of the four numbers look better, and the user is in the same position as before. The one line that gives the split away is the count: 16 tickets for the same 15 problems. Closing a ticket too early is the other common pull: mean time to resolve drops because the fix never had to land, and the reopen rate climbs when the fault comes back. Read the metrics together and beside the ticket count. A falling mean time to resolve beside a rising reopen rate is a desk closing too soon, not a desk working fast.
Reading the dashboard, not the user
Wrong: “first-contact resolution is up ten points this week, keep doing what you did”. Right: compare the same weeks on reopen rate, backlog age and the number of tickets for each problem reported. If the reopen rate rose, or the tickets grew faster than the problems, the gain is in the bookkeeping, not in the service.
Checkpoint
Check: Reading metrics together
Practise: a desk that also opens on Saturday mornings
The program in this lesson closes its desk at weekends. The exercise moves to a desk whose published week adds Saturday
from 09:00 to 13:00, and asks you to write add_business_minutes for that week, with holidays, a UTC input and a
different week passed in as an argument. Its sample tests start on a Friday evening, on a Saturday before and after its
early close, on a Sunday and before opening, so a copy of the lesson’s function fails several of them.
Exercise · Medium · Python
Count business minutes for a desk that opens on Saturday mornings
The lesson's desk is closed at weekends. This desk publishes a different week: 09:00 to 18:00 IST from Monday to Friday, 09:00 to 13:00 IST on Saturday, and closed on Sunday. Write add_business_minutes(start, minutes, holidays=(), week=WEEK) for it. It returns the moment that is minutes business minutes after start, so the desk can promise a deadline that lands on a minute it is open.
Arguments:
start: a timezone-awaredatetime. Read it on the desk's clock first,start.astimezone(IST), whatever offset it carries.minutes: a whole number of business minutes to add, 0 or more.holidays: a collection ofdatevalues on which the desk is closed all day, whatever the week says.week: adictfrom a weekday number (0for Monday to6for Sunday, asdatetime.weekday()numbers them) to an(open, close)pair oftimevalues. A weekday missing from the dict is a closed day. The starter definesWEEKfor this desk, and the tests also pass a week of their own.
Rules the function enforces:
- Only open minutes count. Each day counts the minutes between its own open and close, so a Saturday counts 240 at most and a Sunday none.
- Starts outside the open hours move forward. A start before a day's open uses that open. A start at or after a day's close, on a closed day or on a holiday moves to the open of the next day that is open.
- **The result is a timezone-aware
datetimein IST**, between a day's open and its close (a deadline may fall exactly at a close). - **A
startwithouttzinforaisesValueError**, because a bare clock time names no single moment.
The starter already imports what you need and defines IST, UTC and WEEK. Write the body of add_business_minutes so every sample test passes.
Starter code · add_business_minutes.py
from datetime import date, datetime, time, timedelta, timezone
IST = timezone(timedelta(hours=5, minutes=30)) # Asia/Kolkata, +05:30
UTC = timezone.utc
# This desk's published week: weekday number (Monday is 0) -> (open, close) in IST.
# Sunday (6) is not in the dict, so the desk is closed that day.
WEEK = {
0: (time(9, 0), time(18, 0)),
1: (time(9, 0), time(18, 0)),
2: (time(9, 0), time(18, 0)),
3: (time(9, 0), time(18, 0)),
4: (time(9, 0), time(18, 0)),
5: (time(9, 0), time(13, 0)),
}
def add_business_minutes(start, minutes, holidays=(), week=WEEK):
"""Return the moment that is `minutes` business minutes after `start`.
TODO: read `start` on the IST clock, then spend the minutes inside the
open hours of each day in `week`, skipping closed days and holidays.
Raise ValueError when `start` has no tzinfo.
"""
return start The sample tests · test_add_business_minutes.py
from datetime import date, datetime, time, timedelta
from add_business_minutes import IST, UTC, add_business_minutes
def test_ten_minutes_in_the_middle_of_a_monday():
"""ten business minutes added at 10:00 on a Monday end at 10:10"""
start = datetime(2026, 10, 12, 10, 0, tzinfo=IST)
assert add_business_minutes(start, 10) == datetime(2026, 10, 12, 10, 10, tzinfo=IST)
def test_friday_evening_carries_into_saturday_morning():
"""30 minutes fit before the Friday close; the other 30 are spent from the Saturday open"""
start = datetime(2026, 10, 9, 17, 30, tzinfo=IST)
assert add_business_minutes(start, 60) == datetime(2026, 10, 10, 9, 30, tzinfo=IST)
def test_saturday_closes_at_one():
"""Saturday 12:30 + 60 minutes: 30 before the 13:00 close, Sunday is closed, 30 on Monday"""
start = datetime(2026, 10, 10, 12, 30, tzinfo=IST)
assert add_business_minutes(start, 60) == datetime(2026, 10, 12, 9, 30, tzinfo=IST)
def test_a_full_saturday_ends_at_its_close():
"""the 240 minutes of a Saturday end exactly at 13:00"""
start = datetime(2026, 10, 10, 9, 0, tzinfo=IST)
assert add_business_minutes(start, 240) == datetime(2026, 10, 10, 13, 0, tzinfo=IST)
def test_a_start_after_the_saturday_close_waits_for_monday():
"""a start at 14:00 on a Saturday counts nothing until the Monday open"""
start = datetime(2026, 10, 10, 14, 0, tzinfo=IST)
assert add_business_minutes(start, 15) == datetime(2026, 10, 12, 9, 15, tzinfo=IST)
def test_a_holiday_friday_hands_the_minutes_to_saturday():
"""Thursday 17:30 + 60 with the Friday a holiday ends on Saturday at 09:30 on this desk"""
start = datetime(2026, 10, 1, 17, 30, tzinfo=IST)
end = add_business_minutes(start, 60, holidays={date(2026, 10, 2)})
assert end == datetime(2026, 10, 3, 9, 30, tzinfo=IST)
def test_a_holiday_saturday_is_skipped():
"""Friday 17:00 + 120 with the Saturday a holiday: 60 on Friday, 60 on Monday"""
start = datetime(2026, 10, 9, 17, 0, tzinfo=IST)
end = add_business_minutes(start, 120, holidays={date(2026, 10, 10)})
assert end == datetime(2026, 10, 12, 10, 0, tzinfo=IST)
def test_a_sunday_start_rolls_to_the_monday_open():
"""Sunday is not in the week, so the clock starts at 09:00 on Monday"""
start = datetime(2026, 10, 11, 10, 0, tzinfo=IST)
assert add_business_minutes(start, 60) == datetime(2026, 10, 12, 10, 0, tzinfo=IST)
def test_zero_minutes_on_a_sunday_land_on_the_monday_open():
"""zero minutes still move a closed-day start to the next working minute"""
start = datetime(2026, 10, 11, 10, 0, tzinfo=IST)
assert add_business_minutes(start, 0) == datetime(2026, 10, 12, 9, 0, tzinfo=IST)
def test_before_opening_uses_the_day_open():
"""a start at 07:15 on a Monday starts the clock at 09:00"""
start = datetime(2026, 10, 12, 7, 15, tzinfo=IST)
assert add_business_minutes(start, 90) == datetime(2026, 10, 12, 10, 30, tzinfo=IST)
def test_a_utc_input_is_read_in_ist():
"""10:00 UTC on a Wednesday is 15:30 on the desk's clock; the answer comes back in IST"""
start = datetime(2026, 10, 7, 10, 0, tzinfo=UTC)
end = add_business_minutes(start, 45)
assert end == datetime(2026, 10, 7, 16, 15, tzinfo=IST)
assert end.utcoffset() == timedelta(hours=5, minutes=30)
def test_the_week_argument_is_used():
"""a desk open 10:00 to 16:00 on weekdays only: Friday 15:30 + 60 ends on Monday at 10:30"""
weekdays_only = {day: (time(10, 0), time(16, 0)) for day in range(5)}
start = datetime(2026, 10, 9, 15, 30, tzinfo=IST)
end = add_business_minutes(start, 60, week=weekdays_only)
assert end == datetime(2026, 10, 12, 10, 30, tzinfo=IST)
def test_naive_datetime_is_rejected():
"""a datetime without tzinfo cannot be read on the IST clock safely"""
try:
add_business_minutes(datetime(2026, 10, 12, 10, 0), 10)
except ValueError:
return
raise AssertionError("ValueError not raised for a naive datetime") A hint
Saturday is where the lesson's program goes wrong for this desk: it gives every open day the same hours and treats Saturday as closed. Look the hours up for each day inside the loop, from week, instead of once before it, and let a day that week does not list behave like a holiday.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then test yourself across the five ideas, from the ticket states to the metrics that mislead; the questions adapt to your answers as you go:
Practice that adapts to you
Practise this lesson
10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): a VIP marks a ticket closed on their own saying ‘it works now’. The error returns ten minutes later. What state do you move it to, and why not open a fresh one?
Reopen the related ticket, so the state moves from closed back to in progress and the history travels with the work. A duplicate loses the context: the error text, the actions already tried, the notes about who was contacted. It also makes the fault look rarer than it is and counts the second contact as first-contact resolution, which flatters the dashboard without helping the user. Keep the resolution window counting from the first contact when the SLA says reopens do, so the agreement does not reset by accident, and tell the user plainly that you are reopening the earlier ticket.
Two harder cases begin where this lesson stops, one on priority and one on a customer’s time zone. Work out your answer on paper before you open the first step of each:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
At 10:00 a sales manager's laptop will not start; she presents to a client at 10:40. A team of six finds its shared drive read-only, but local copies keep them working until tomorrow's 10:00 submission. On the lesson's 3x3 matrix, which goes first, what do you tell the team, and when do you look again?
-
Mid to senior
Your desk works 09:00 to 18:00 IST, Monday to Friday, and promises P2 resolution in 4 business hours. A P2 ticket from a New York customer is raised at
2026-03-06T16:00-05:00. When is the deadline on the desk's clock, and what time do you give the customer for it? Show your working.
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Which six states does a ticket move through, and what happens when the fault returns?
New, assigned, in progress, pending, resolved and closed. A related fault that comes back reopens the ticket instead of starting a new one, so the history travels with the work.
-
A VIP closes their own ticket and the error is back ten minutes later. Why reopen it rather than open a fresh one?
A duplicate loses the error text, the actions tried and the contact notes. It also makes the fault look rarer and counts the second contact as first-contact resolution, which flatters the dashboard without helping the user.
-
How is a ticket's priority set?
From two readings, impact and urgency, looked up on the matrix the organisation has published. Changing the priority is a decision with its reason in the ticket, not a judgement about the caller.
-
How does an SLA clock count toward a resolution target?
In business minutes only, against the published hours and the holiday calendar. On a desk open 09:00 to 18:00 on weekdays, 60 minutes from Thursday 17:30+05:30, with the Friday a holiday, end on Monday at 09:30+05:30.
-
A customer gives a time with no offset. What do you do before adding SLA minutes to it?
Treat it as a question, not a start. Once the zone is known, convert the time to the desk's clock with a time-zone library, because the UK, Europe and most of the US move their clocks.
-
Why read service desk metrics together rather than one at a time?
Each one can be gamed alone, and a split ticket or an early close moves several at once. Read first-contact resolution, mean time to resolve, reopen rate and backlog age together, beside the ticket count.
Key takeaways
- A ticket moves through six states (new, assigned, in progress, pending, resolved, closed), and a related fault that returns is a reopen, not a new ticket, so the history travels with the work.
- Priority is impact and urgency read off the organisation’s published 3x3 matrix; on the sample matrix a stopped payroll server is P1 and a VIP’s forgotten password is P3, whoever called first.
- A resolution target counts business minutes against the published hours and the holiday calendar, so 60 minutes from Thursday 17:30+05:30, with the Friday a holiday, end on Monday at 09:30+05:30.
- Convert a customer’s time to the desk’s clock with a time-zone library before adding minutes, because the UK, Europe and most of the US move their clocks; a time with no offset is a question, not a start.
- Each desk metric can be gamed on its own, so read first-contact resolution, mean time to resolve, reopen rate and backlog age together and beside the ticket count: a split ticket or an early close moves several of them at once.
Terms in this lesson
- backlog age
- The average age of the tickets that are still open at the moment the report is run. Measured in hours or days; it moves only when tickets are opened, closed or stay open longer, not when a closed ticket is split.
- business-hours calendar
- The published daily working hours of a service desk, such as 09:00 to 18:00 Monday to Friday. Service-level agreement clocks count minutes only inside these windows, so a wall-clock promise is replaced with a business-hours one.
- first-contact resolution
- The share of closed tickets that were resolved during the first contact with the user, with no later work needed. Written FCR. Easy to flatter by splitting one ticket into two or by closing early.
- holiday calendar
- The list of dates on which an office is closed and no business minutes elapse, such as a country's public holidays or an organisation's shutdowns. The service-level agreement reads it beside the business hours.
- impact
- In service-desk work: how many people a fault affects and how critical the service it stops is. One axis of the priority matrix: low, medium or high.
- mean time to resolve
- The average time between a ticket's creation and its resolution, taken over the closed tickets in a window. Written MTTR. Falls alongside a rising reopen rate when tickets are being closed too soon.
- pending state
- The state a ticket moves to while the owner is waiting on a reply, usually from the user or a vendor. The service-level agreement clock pauses while a ticket is pending only if the agreement itself names that rule.
- priority matrix
- A 3x3 table that reads impact against urgency and prints the priority band (such as P1 to P4) for each cell. Every organisation publishes its own matrix in its service-level agreement.
- reopen rate
- The share of closed tickets that had to be reopened within a window, usually a few days. A rising reopen rate beside a falling mean time to resolve is the signature of a desk closing tickets too early.
- resolution target
- How soon, in business hours, a service-level agreement commits the fault to be fixed for a ticket of a given priority. Measured against the business-hours calendar and the holiday calendar for the office.
- response target
- How soon, in business hours, a service-level agreement commits a human reply to the user after a ticket of a given priority is raised. The response clock stops at the first substantial reply.
- service-level agreement
- A written contract between the service desk and its users or customers, naming the targets the desk commits to for each priority (response time, resolution time) and the business hours and holidays those targets are measured against. Its short name is SLA.
- urgency
- In service-desk work: how fast the harm from a fault grows if it is left in place. The other axis of the priority matrix: low (no deadline), medium (today) or high (minutes to harm).
References
- RFC 3339: Date and Time on the Internet: Timestamps (Internet Engineering Task Force (IETF))
- ISO 8601 date and time format (International Organization for Standardization (ISO))
- Time Zone Database: the asia file (Asia/Kolkata) (Internet Assigned Numbers Authority (IANA))
- Time Zone Database: the europe file (daylight saving in the UK) (Internet Assigned Numbers Authority (IANA))
- Time Zone Database: the northamerica file (daylight saving in the US) (Internet Assigned Numbers Authority (IANA))
- CompTIA A+ Core 2 (220-1202) V15 exam objectives (CompTIA)
- ITIL catalogue entry (PeopleCert)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress