Advanced guides

Demands and shifts

Call-centers and retailers typically does not have fixed shifts. They have demands such as

  1. (Some)one on duty between 8 AM to 8 PM
  2. (Some)one on duty between 9 AM and 11 AM

This demand could be covered by two employees or by three employees. In the latter case, two employees would share the first demand by covering two 6-hour shifts.

Our API lets you schedule in a demand-based way easily.

  1. Turn your demand into continuous shifts of one hour.
  2. Use the same spot for every one of these shifts and include that spot in allowedCollisionSpots, see below.
  3. Change the settings to allow multiple shifts a day, see below.

It is important that you base your requests of the settings below to avoid scenarios where an employee work for just one hour one a given day!

We use shifts in this article differently than we have done in the rest of this documentation. The default settings are great for default use-cases but if you do demand-based scheduling you have to change the default settings.

Breaking demands into shifts

Here is an example in Kotlin of how you could turn a daily demand one person being present from 8 AM to 6 PM each day. If you are unsure about the fields here, consult the documentation for shifts.

This code creates a week-long schedule of hourly shifts for two alternating spots with shifts running from 8 AM to 6 PM each day. The main logic is in a nested loop structure.

The outer loop iterates over 7 days (1 until 8).

  • For each day, it creates 6 spots.
  • For each spot, it creates shifts for every hour from 8 AM to 6 PM (10 hours).
val tz = TimeZone.of("Europe/Copenhagen")

val startDate = LocalDateTime.parse("2024-07-28T08:00:00")

val shifts = (1 until 8).flatMap { day ->
    val currentDate = startDate.toInstant(tz).plus(day, DateTimeUnit.DAY, tz)
    (1..6).flatMap { spotNumber ->
        (8 until 18).map { hour -> // Range from 8 AM to 6 PM (which is maxHoursDay)
            val start = currentDate.plus(hour - 8, DateTimeUnit.HOUR, tz)
            val end = currentDate.plus(hour - 7, DateTimeUnit.HOUR, tz) // Each shift lasts 1 hour
            Shift(
                id = (day * 1000 + hour * 10 + spotNumber),
                spot = (spotNumber % 2) + 1,
                start = start,
                end = end,
                importance = StaffingMode.MANDATORY,
                allowedCollisionSpots = listOf((spotNumber % 2) + 1),
                fixed = false,
                historic = false,
                employee = null,
            )
        }
    }
}

Changing the settings

Changing the settings like so would be a good starting point. To get help, you can always contact us at [email protected] or revisit the section on Settings.

@Serializable
data class Setting(
    var firstPriorityWeight: Int,
    var secondPriorityWeight: Int,
    var thirdPriorityWeight: Int,
    var fourthPriorityWeight: Int
)

payload = (
    employees = listOf(), // Add employees here
    shifts = shifts, // As defined above
    spots = listOf(), // Make sure to include spot id=1 and id=2
    numberOfAllowedShiftCollisions = 1000,
    settings = mapOf(
        "distributeShiftsEvenly" to Setting(0, 0, 0, 10),
        "minimizeEmployeeUsage" to Setting(0, 0, 100, 0),
        "employeeHourlyPatternCompactness" to Setting(0, 20, 0, 0),
        "atLeastTwoContinousDaysOffAWeek" to Setting(0, 0, 0, 0),
        "maxConsecutiveLateShifts" to Setting(0,0,0,0),
        "maxConsecutiveNightShifts" to Setting(0,0,0,0),
        "maxConsecutiveWorkingDays" to Setting(0,0,0,0),
        "minConsecutiveWorkingDays" to Setting(0,0,0,0),
        "noConsecutiveFiveWorkdayWeeks" to Setting(0,0,0,0),
        "sameTimeOfDayForConsecutiveShifts" to Setting(0,0,0,0),
        "singleShiftPerDay" to Setting(0,0,0,0),
        "employeeShiftCompactness" to Setting(0,0,0,0),
    )
)

A full example

Here is a full example including Kotlin data classes

@Serializable
data class ConsumerRequestPayload(
    val employees: List<ConsumerEmployee>,
    val shifts: List<ConsumerShiftRequest>,
    val spots: List<ConsumerSpot>,
    val numberOfAllowedShiftCollisions: Int,
    val settings: Map<String, Setting>,
    val callbackUrl: String,
    val callbackAuthenticationHeaderName: String,
    val callbackAuthenticationHeaderValue: String
)

@Serializable
data class Setting(
    var firstPriorityWeight: Int,
    var secondPriorityWeight: Int,
    var thirdPriorityWeight: Int,
    var fourthPriorityWeight: Int
)

@Serializable
data class Spot(
    val id: Int,
    val requiresRestAfterShiftHere: Boolean = false,
    val singleEmployeePerWeekPreferred: Boolean = false,
    val teams: List<Int>,
    val skills: List<Int>,
    val foreignInt: Int? = null
)

@Serializable
data class Shift(
    val id: Int,
    val spot: Int,
    @Serializable(with = InstantIso8601Serializer::class)
    val start: Instant,
    @Serializable(with = InstantIso8601Serializer::class)
    val end: Instant,
    val importance: StaffingMode,
    val suggestedEmployees: List<Int> = listOf(),
    val allowedCollisionSpots: List<Int> = listOf(),
    val fixed: Boolean,
    val historic: Boolean,
    var employee: Int?,
    val foreignInt: Int? = null
)

@Serializable
data class Employee(
    val id: Int,
    val teams: List<Team>,
    val skills: List<Skill>,
    val contract: Contract,
    val wishes: List<Wish>
)

@Serializable
data class Team(
    val id: Int,
    @Serializable(with = InstantIso8601Serializer::class)
    val start: Instant? = null,
    @Serializable(with = InstantIso8601Serializer::class)
    val end: Instant? = null,
)

@Serializable
data class Skill(
    val id: Int,
    @Serializable(with = InstantIso8601Serializer::class)
    val start: Instant? = null,
    @Serializable(with = InstantIso8601Serializer::class)
    val end: Instant? = null,
)

@Serializable
data class Contract(
    val hoursBetweenShift: Int,
    val maxHoursDay: Int,
    val maxHoursWeek: Int,
    val maxHoursMonth: Int,
    val maxWorkDaysMonth: Int,
    val maxConsecutiveDays: Int,
    val minConsecutiveDays: Int,
    val weekendWorkFrequency: Int,
    val maxNhours: Int,
    val InKWeeks: Int,
    val maxConsecutiveLateShifts: Int,
    val maxConsecutiveNightShifts: Int
)

@Serializable
data class Wish(
    val type: WishTypeEnum,
    @Serializable(with = InstantIso8601Serializer::class)
    val start: Instant,
    @Serializable(with = InstantIso8601Serializer::class)
    val end: Instant,
)

val tz = TimeZone.of("Europe/Copenhagen")

val startDate = LocalDateTime.parse("2024-07-28T08:00:00")

val defaultContract = Contract(
    hoursBetweenShift = 11,
    maxHoursDay = 10,
    maxHoursWeek = 40,
    maxHoursMonth = 40 * 4,
    maxWorkDaysMonth = 30,
    maxConsecutiveDays = 30,
    minConsecutiveDays = 1,
    weekendWorkFrequency = 1,
    maxNhours = 40,
    InKWeeks = 1,
    maxConsecutiveLateShifts = 100,
    maxConsecutiveNightShifts = 100,
)

private val payload = Payload(
    employees = (1 until 10).map { index ->
        Employee(
            id = index,
            teams = listOf(Team((index % 2) + 1)),
            skills = listOf(Skill((index % 2) + 1)),
            contract = defaultContract,
            wishes = listOf(
                Wish(
                    // Every one has a full day in week 1
                    type = "UNAVAILABLE",
                    start = startDate.toInstant(tz).minus(8, DateTimeUnit.HOUR, tz)
                        .plus(index % 5, DateTimeUnit.DAY, tz),
                    end = startDate.toInstant(tz).plus(16, DateTimeUnit.HOUR, tz)
                        .plus(index % 5, DateTimeUnit.DAY, tz),
                )
            ),
        )
    },
    spots = listOf(
        Spot(id = 1, singleEmployeePerWeekPreferred = true, teams = listOf(1), skills = listOf(1)),
        Spot(id = 2, singleEmployeePerWeekPreferred = true, teams = listOf(2), skills = listOf(2)),
    ),
    shifts = (1 until 8).flatMap { day ->
        val currentDate = startDate.toInstant(tz).plus(day, DateTimeUnit.DAY, tz)
        (1..6).flatMap { spotNumber ->
            (8 until 18).map { hour -> // Range from 8 AM to 6 PM (which is maxHoursDay)
                val start = currentDate.plus(hour - 8, DateTimeUnit.HOUR, tz)
                val end = currentDate.plus(hour - 7, DateTimeUnit.HOUR, tz) // Each shift lasts 1 hour
                Shift(
                    id = (day * 1000 + hour * 10 + spotNumber),
                    spot = (spotNumber % 2) + 1,
                    start = start,
                    end = end,
                    importance = StaffingMode.MANDATORY,
                    allowedCollisionSpots = listOf((spotNumber % 2) + 1),
                    fixed = false,
                    historic = false,
                    employee = null,
                )
            }
        }
    },
    numberOfAllowedShiftCollisions = 1000,
    settings = mapOf(
        "distributeShiftsEvenly" to Setting(0, 0, 0, 10),
        "minimizeEmployeeUsage" to Setting(0, 0, 100, 0),
        "employeeHourlyPatternCompactness" to Setting(0, 20, 0, 0),
        "atLeastTwoContinousDaysOffAWeek" to Setting(0, 0, 0, 0),
        "maxConsecutiveLateShifts" to Setting(0,0,0,0),
        "maxConsecutiveNightShifts" to Setting(0,0,0,0),
        "maxConsecutiveWorkingDays" to Setting(0,0,0,0),
        "minConsecutiveWorkingDays" to Setting(0,0,0,0),
        "noConsecutiveFiveWorkdayWeeks" to Setting(0,0,0,0),
        "sameTimeOfDayForConsecutiveShifts" to Setting(0,0,0,0),
        "singleShiftPerDay" to Setting(0,0,0,0),
        "employeeShiftCompactness" to Setting(0,0,0,0),
    )
    callbackUrl = "https://www.yourdomain.app/custom_callback_url",
    callbackAuthenticationHeaderName = "X-Api-Key",
    callbackAuthenticationHeaderValue = "SECRET"
)
Previous
Overlapping shifts