200ok shiftplanning - Optimize your work force schedule

Table of Contents

200ok.svg

1. 200ok shiftplanning

This is the service documentation for the 200ok Gmbh product 200ok shiftplanning. It is intended for developers working with or on 200ok shiftplanning.

1.1. Feedback

Please let us know about gaps or errors in our documentation at info@200ok.ch.

2. Introduction

200ok shiftplanning is a modern data-driven shift planning solution that drives the efficiency of it's customers through its stateless API solution.

The concept of 200ok shiftplanning is quite easy: Given a set of credentials and a configured tenant, you can generate a Roster with planned shifts in just one single API call.

200ok has been founded by two former lecturers of the university of applied sciences of Zurich. During this time, they have acquired a lot of knowledge in the area of NP hard and NP complete problems and have supervised a series of related papers. Some of these papers are:

  • Evaluation of solving the nurse-rostering-problem in web applications
  • An explorative study into the realm of On-Device Machine Training
  • Linked data-driven UI for task-based computing
  • Comparing traditional statistical methods with machine learning
  • Generic API to process fundamental computer science problems
  • Evaluating concepts of NP-complete problems for web applications
  • Evaluation on bringing business rules from customers into a web application

Apart from these papers, 200ok has been involved with providing products for shift planning since 2014 and hence has a lot of practical experience on the topic. 200ok shiftplanning is the culmination of decades of experience solving hard business problems and data driven API development.

2.1. Overview Domain Model

The following list gives a rough overview over the domain model. The application of the models depends on the specific use case.

  • Roster (a list of employees and associated information for a given time period)
  • Shift (a scheduled period of work or duty)
  • Employee
  • Spot (a work space, area, customer, or subject)
  • Skill (the ability or permission to perform work in a given shift or spot)
  • Availability (of an employee)
  • Contract (time constraints for an employee)

erd.svg

Figure 1: An entity relationship diagram showing the domain model

3. Use cases

To optimize a Roster, you create a Job of a specific type. The type depends on what resource you want to optimize for.

3.1. Optimize for employees (Classic shift operation)

In a classic shift operation, during optimization, shifts are assigned to employees. Hence, the shiftplanning job type is called optimize_for_employees. For an example how to create a roster for this, see create roster.

3.1.1. Rules

To calculate a Roster with planned shifts, there have to be rules. Here is the list of rules that 200ok shiftplanning uses by for Job type optimize_for_employees. These rules can, of course, be adapted to your specific business and to your wishes. We are also happy to listen to your requirements and add rules specific to your business.

Rules are always formulated for the negative case. The reason is that our algorithm will give penalty points for every rule (constraint) that cannot be fulfilled.

- employee lacks required skill for shift
- employee lacks required skill for spot
- employee has overlapping shifts
- daily minutes exceed contract maximum
- weekly minutes exceed contract maximum
- monthly minutes exceed contract maximum
- yearly minutes exceed contract maximum
- shift remains unassigned
- locked employee was changed
- employee assigned to shift but unavailable
- weekly minutes exceed skill maximum
- employee assigned to shift undesired
- employee assigned to shift desired

3.1.2. Budgeting per Skill

Optionally, if you want employees to work a maximum of time on a certain task, you can set a time budget per Skill.

Example use-case: A support department wants their employees to work on different Spot\s for a maximum amount of time - 2 days a week on the hotline, 2 days a week on second-level support and finally one day a week giving trainings to customers. Achieve this kind of budgeting by setting maximumMinutesPerWeekPerEmployee on the relevant Skill.

3.2. Optimize for spots (Service provider)

Service providers classically aren't shift operations. However, they have customers with budgets. The problem during shift planning is to assign the shifts to those customers (i.e. campaigns, locations, etc.). This can be achieved by providing Shifts that have pre-assigned Employees, but are lacking the Spot. Spots have a time budget assigned through maximumMinutes. The shiftplanning job type is called optimize_for_spots.

Here is a full example on how to create a roster with optimize_for_spots.

3.2.1. Create Roster

Here's an example Roster that has pre-assigned shifts that are lacking a Spot.

{
  "meta": {
    "ruleFilePaths": [
      "rosterConstraintsOptimizeForSpot.drl"
    ],
    "secondsSpentLimit": 10
  },
  "skills": [
    {
      "id": "reiskochen",
      "name": "Reis kochen"
    },
    {
      "id": "reiswaschen",
      "name": "Reis waschen"
    }
  ],
  "spots": [
    {
      "id": "reiskocher",
      "name": "Reiskocher",
      "maximumMinutes": 180,
      "requiredSkills": [
	"reiskochen"
      ]
    },
    {
      "id": "lavabo",
      "name": "Lavabo",
      "maximumMinutes": 240,
      "requiredSkills": [
	"reiswaschen"
      ]
    }
  ],
  "contracts": [
    {
      "id": "fulltime",
      "name": "Fulltime",
      "maximumMinutesPerDay": 0,
      "maximumMinutesPerWeek": 2550,
      "maximumMinutesPerMonth": 0,
      "maximumMinutesPerYear": 0
    }
  ],
  "employees": [
    {
      "id": "mrsrice",
      "name": "Mrs Rice",
      "contract": "fulltime",
      "skills": [
	"reiskochen",
	"reiswaschen"
      ]
    },
    {
      "id": "mrrice",
      "name": "Mr Rice",
      "contract": "fulltime",
      "skills": [
	"reiswaschen"
      ]
    }
  ],
  "shifts": [
    {
      "id": "82c397c6-8d58-4329-9c94-69d3637b6b1b",
      "employee": "mrsrice",
      "startDateTime": "2021-06-23T08:00",
      "endDateTime": "2021-06-23T12:00"
    },
    {
      "id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7",
      "employee": "mrsrice",
      "startDateTime": "2021-06-23T14:00",
      "endDateTime": "2021-06-23T17:00"
    },
    {
      "id": "5eb45659-f88f-4634-a925-3d5ef706199f",
      "employee": "mrrice",
      "startDateTime": "2021-06-23T08:00",
      "endDateTime": "2021-06-23T12:00"
    }
  ]
}
echo $example_roster |  http $host/v1/tenants/$tenant_id/jobs \
			     Authorization:"Token ${auth_token}" \
			     webhook==$webhook_url \
			     type==optimize_for_spots
{"msg":"Job accepted.","webhook":"http://your-domain.ch/webhook","job_id":"875016f8-41fb-4ad2-851f-59900b471749"}

When the job is finished, the resulting Job will be POSTed to http://your-domain.ch/webhook. It will include some metadata. The actual Roster will be nested under the key results. The Roster, again, will include some metadata and planned shifts indicated by the employee id per Shift.

This is what it looks like:

{
  "payload": {
    "meta": {
      "ruleFilePaths": [
	"rosterConstraintsOptimizeForSpot.drl"
      ],
      "secondsSpentLimit": 10
    },
    "spots": [
      {
	"id": "reiskocher",
	"name": "Reiskocher",
	"maximumMinutes": 180,
	"requiredSkills": [
	  "reiskochen"
	]
      },
      {
	"id": "lavabo",
	"name": "Lavabo",
	"maximumMinutes": 240,
	"requiredSkills": [
	  "reiswaschen"
	]
      }
    ],
    "shifts": [
      {
	"id": "82c397c6-8d58-4329-9c94-69d3637b6b1b",
	"employee": "mrsrice",
	"endDateTime": "2021-06-23T12:00",
	"startDateTime": "2021-06-23T08:00"
      },
      {
	"id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7",
	"employee": "mrsrice",
	"endDateTime": "2021-06-23T17:00",
	"startDateTime": "2021-06-23T14:00"
      },
      {
	"id": "5eb45659-f88f-4634-a925-3d5ef706199f",
	"employee": "mrrice",
	"endDateTime": "2021-06-23T12:00",
	"startDateTime": "2021-06-23T08:00"
      }
    ],
    "skills": [
      {
	"id": "reiskochen",
	"name": "Reis kochen"
      },
      {
	"id": "reiswaschen",
	"name": "Reis waschen"
      }
    ],
    "contracts": [
      {
	"id": "fulltime",
	"name": "Fulltime",
	"maximumMinutesPerDay": 0,
	"maximumMinutesPerWeek": 2550,
	"maximumMinutesPerYear": 0,
	"maximumMinutesPerMonth": 0
      }
    ],
    "employees": [
      {
	"id": "mrsrice",
	"name": "Mrs Rice",
	"skills": [
	  "reiskochen",
	  "reiswaschen"
	],
	"contract": "fulltime"
      },
      {
	"id": "mrrice",
	"name": "Mr Rice",
	"skills": [
	  "reiswaschen"
	],
	"contract": "fulltime"
      }
    ]
  },
  "started_at": "2022-01-25T15:33:08.196Z",
  "completed_at": "2022-01-25T15:33:20.767Z",
  "webhook": "https://webhook.site/e67d9837-5ae0-468f-8e04-04bf34bc87ea",
  "indictment": {
    "charge-0": {
      "justification": {
	"id": "5eb45659-f88f-4634-a925-3d5ef706199f",
	"spot": null,
	"startDateTime": "2021-06-23T08:00",
	"endDateTime": "2021-06-23T12:00",
	"requiredSkills": null,
	"employee": {
	  "id": "mrrice",
	  "name": "Mr Rice",
	  "contract": {
	    "id": "fulltime",
	    "name": "Fulltime",
	    "maximumMinutesPerDay": 0,
	    "maximumMinutesPerWeek": 2550,
	    "maximumMinutesPerMonth": 0,
	    "maximumMinutesPerYear": 0
	  },
	  "skills": [
	    {
	      "id": "reiswaschen",
	      "name": "Reis waschen"
	    }
	  ]
	},
	"lockedEmployee": null
      },
      "constraintMatchSet": [
	{
	  "constraintName": "spotRemainsUnassigned",
	  "justificationList": [
	    {
	      "id": "5eb45659-f88f-4634-a925-3d5ef706199f",
	      "spot": null,
	      "startDateTime": "2021-06-23T08:00",
	      "endDateTime": "2021-06-23T12:00",
	      "requiredSkills": null,
	      "employee": {
		"id": "mrrice",
		"name": "Mr Rice",
		"contract": {
		  "id": "fulltime",
		  "name": "Fulltime",
		  "maximumMinutesPerDay": 0,
		  "maximumMinutesPerWeek": 2550,
		  "maximumMinutesPerMonth": 0,
		  "maximumMinutesPerYear": 0
		},
		"skills": [
		  {
		    "id": "reiswaschen",
		    "name": "Reis waschen"
		  }
		]
	      },
	      "lockedEmployee": null
	    }
	  ],
	  "score": {
	    "initScore": 0,
	    "hardScore": 0,
	    "softScore": -240,
	    "feasible": true,
	    "solutionInitialized": true
	  },
	  "constraintId": "defaultpkg/spotRemainsUnassigned"
	}
      ],
      "score": {
	"initScore": 0,
	"hardScore": 0,
	"softScore": -240,
	"feasible": true,
	"solutionInitialized": true
      },
      "constraintMatchCount": 1
    }
  },
  "type": "optimize_for_spots",
  "state": "complete",
  "id": 142,
  "tenant_id": 1,
  "uuid": "b50d871c-2194-49c1-8d6a-6a2ce78fb2dc",
  "created_at": "2022-01-25T15:33:08.169Z",
  "results": [
    {
      "id": 197,
      "job_id": 142,
      "payload": {
	"meta": {
	  "entityClass": "shift_for_spot",
	  "ruleFilePaths": [
	    "rosterConstraintsOptimizeForSpot.drl"
	  ],
	  "secondsSpentLimit": 10
	},
	"score": {
	  "feasible": true,
	  "hardScore": 0,
	  "initScore": 0,
	  "softScore": -240,
	  "solutionInitialized": true
	},
	"spots": [
	  {
	    "id": "reiskocher",
	    "name": "Reiskocher",
	    "maximumMinutes": 180,
	    "requiredSkills": [
	      "reiskochen"
	    ]
	  },
	  {
	    "id": "lavabo",
	    "name": "Lavabo",
	    "maximumMinutes": 240,
	    "requiredSkills": [
	      "reiswaschen"
	    ]
	  }
	],
	"shifts": [
	  {
	    "id": "82c397c6-8d58-4329-9c94-69d3637b6b1b",
	    "spot": "lavabo",
	    "type": "shift_for_spot",
	    "employee": "mrsrice",
	    "endDateTime": "2021-06-23T12:00",
	    "startDateTime": "2021-06-23T08:00",
	    "lockedEmployee": null,
	    "requiredSkills": null
	  },
	  {
	    "id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7",
	    "spot": "reiskocher",
	    "type": "shift_for_spot",
	    "employee": "mrsrice",
	    "endDateTime": "2021-06-23T17:00",
	    "startDateTime": "2021-06-23T14:00",
	    "lockedEmployee": null,
	    "requiredSkills": null
	  },
	  {
	    "id": "5eb45659-f88f-4634-a925-3d5ef706199f",
	    "spot": null,
	    "type": "shift_for_spot",
	    "employee": "mrrice",
	    "endDateTime": "2021-06-23T12:00",
	    "startDateTime": "2021-06-23T08:00",
	    "lockedEmployee": null,
	    "requiredSkills": null
	  }
	],
	"skills": [
	  {
	    "id": "reiskochen",
	    "name": "Reis kochen"
	  },
	  {
	    "id": "reiswaschen",
	    "name": "Reis waschen"
	  }
	],
	"contracts": [
	  {
	    "id": "fulltime",
	    "name": "Fulltime",
	    "maximumMinutesPerDay": 0,
	    "maximumMinutesPerWeek": 2550,
	    "maximumMinutesPerYear": 0,
	    "maximumMinutesPerMonth": 0
	  }
	],
	"employees": [
	  {
	    "id": "mrsrice",
	    "name": "Mrs Rice",
	    "skills": [
	      "reiskochen",
	      "reiswaschen"
	    ],
	    "contract": "fulltime"
	  },
	  {
	    "id": "mrrice",
	    "name": "Mr Rice",
	    "skills": [
	      "reiswaschen"
	    ],
	    "contract": "fulltime"
	  }
	],
	"availabilities": []
      },
      "final": true,
      "created_at": "2022-01-25T15:33:20.757Z"
    }
  ]
}

3.2.2. Rules

To calculate a Roster with planned shifts, there have to be rules. Here is the list of rules that 200ok shiftplanning uses by for Job type optimize_for_spots. These rules can, of course, be adapted to your specific business and to your wishes. We are also happy to listen to your requirements and add rules specific to your business.

Rules are always formulated for the negative case. The reason is that our algorithm will give penalty points for every rule (constraint) that cannot be fulfilled.

- employee lacks required skill for shift
- employee lacks required skill for spot
- minutes exceed spot maximum
- weekly minutes exceed skill maximum
- spot remains unassigned
- employee assigned to shift undesired
- employee assigned to shift desired

3.3. Optional rules

There are some rules that can optionally be enabled for every use case:

- context switch on same day

3.4. Description of the rules

There are hard and soft constraints. The algorithm primarily optimizes for hard constraints and only secondary for soft constraints. If any one hard constraint is not met, the solution will be marked as infeasible.

3.4.1. Context switch on same day

Employees should not need to switch spots within a day. Switching spots is considered a context switch, which in some cases should be minimized.

A match yields soft penalty points equal to the mean minutes of conflicting shifts.

3.4.2. Daily Minutes exceed contract maximum

If the employee's contract defines a maximumMinutesPerDay > 0, the sum of all shifts within a day assigned to the employee must not exceed the given number.

A match yields hard penalty points equal to the excess minutes divided by the number of affected shifts.

3.4.3. Weekly minutes exceed contract maximum

If the employee's contract defines a maximumMinutesPerWeek > 0, the sum of all shifts within a week assigned to the employee must not exceed the given number.

A match yields hard penalty points equal to the excess minutes divided by the number of affected shifts.

3.4.4. Monthly minutes exceed contract maximum

If the employee's contract defines a maximumMinutesPerMonth > 0, the sum of all shifts within a month assigned to the employee must not exceed the given number.

A match yields hard penalty points equal to the excess minutes divided by the number of affected shifts.

3.4.5. Yearly minutes exceed contract maximum

If the employee's contract defines a maximumMinutesPerYear > 0, the sum of all shifts within a year assigned to the employee must not exceed the given number.

A match yields hard penalty points equal to the excess minutes divided by the number of affected shifts.

3.4.6. Employee has overlapping shifts

No two shifts assigned to an employee may overlap.

A match yields hard penalty points equal to the sum of minutes of affected shifts.

3.4.7. Employee assigned to shift but unavailable

Employees should not have shifts assigned that conflict with a "unavailability" (an availability of state "UNAVAILABLE").

A match yields hard penalty points equal to the minutes of the violated unavailability.

3.4.8. Employee assigned to shift desired

Employees should have shifts assigned that cover the desired availabilities (availabilities of state "DESIRED").

A match yields soft bonus points equal to the minutes of the affected availability of state DESIRED.

3.4.9. Employee assigned to shift undesired

Employees should not have shifts assigned that cover the unsdesired availability (availabilities of state "UNDESIRED").

A match yields soft penalty points equal to the minutes of the violated availability of state UNDESIRED.

3.4.10. Employee lacks required skill for shift

Employees should not be assigned to shifts that require a certain skill if the employee does not have the skill.

A match yields hard penalty points equal to the minutes of the affected shift.

3.4.11. Employee lacks required skill for spot

Employees should not be assigned to shifts at a given spot if the spot requires a certain skill and the employee does not have the skill.

A match yields hard penalty points equal to the minutes of the affected shift.

3.4.12. Locked employee was changed

If a shift is locked to an employee it should not be assigend to a different employee.

When using the field lockedEmployee on a shift, the same employee must be set on the field employee on that shift.

A match yields hard penalty points equal to the minutes of the affected shift.

3.4.13. Minutes exceed spot maximum

If a spot has maximumMinutes > 0 the sum of shifts in that spot may not exceed the given value.

A match yields hard penalty points equal to the excess minutes.

3.4.14. Shift remains unassigned

Misnomer, better: "Shift remains unassigned to employee"

When optimizing for shifts for employees, no shift should remain unassigned to an employee.

A match yields hard penalty points equal to half the minutes of the affected shift. (The penalty points are half, because an unassigend shift is considered the "best kind of infeasable". E.g. better than an unskilled worker assigned.)

3.4.15. Spot remains unassigned

Misnomer, better: "Shift remains unassigned to spot"

When optimizing shifts for spots, no shift should remain unassigend to a spot. Having said so, shifts that are not assigned to Spots are OK. That only means that there are not enough spots (projects/campaigns/customers/…) with budget available. This rule ensures that Shifts will not be assigned to a Spot if there is no budget available.

A match yields hard penalty points equal to the minutes of the affected shift.

3.4.16. Weekly minutes exceed skill maximum

If a skill has maximumMinutesPerWeekPerEmployee > 0 the sum of all shifts for which the skill is required within a week must not execeed the given number for any employee.

A match yields hard penalty points equal to the excess minutes divided by the number of affected shifts.

4. API

The API of 200ok shiftplanning is documented with Swagger and available at https://compute-demo.200ok.ch.

swagger.png

The following examples use httpie or curl to create http requests. The examples always have two code blocks: First the httpie or curl command, then immediately the result.

4.1. Authentication

This API employs authentication and authorization. You will get appropriate 401 (unauthorized) and 403 (forbidden) status codes. To make authenticated requestes, you first login using basic auth. This will yield a JWT Token that can you will use for subsequent requests in a "Authorization" header.

Here is how to login with basic auth using common commandline tools:

  • curl: curl --basic -u username:password https://compute-demo.200ok.ch/v1/login
  • httpie: http --auth username:password https://compute-demo.200ok.ch/v1/login

If you want to generate a basic auth token for use with this Swagger API, you can use your favorite programming language. Here's an example in shell: echo -n "Basic " $(echo -n "username:password" | base64) This will generate something like Basic dXNlcjI6a29pcmExMg== which you then can use as authorization header to retrieve a JWT token. JWT tokens expire after 2h. Example: curl -H "Authorization: Basic dXNlcjI6a29pcmExMg=" https://compute-demo.200ok.ch/v1/login=

After logging in via basic auth, the retrieved JWT token should be used for endpoints that require authentication. Here's an example:

curl -H "Authorization: Token eyJhbGc...2Bm63IBw" https://compute-demo.200ok.ch/v1/tenants

4.1.1. httpie

http --ignore-stdin --pretty format --auth user1:kissa13 $host/v1/login
{
    "message": "Basic auth succeeded!",
    "user": {
	"id": 1,
	"roles": [
	    "admin",
	    "user"
	],
	"token": "eyJhbGciOiJIUzUxMiJ9.eyJpZCI6MSwicm9sZXMiOlsiYWRtaW4iLCJ1c2VyIl0sImV4cCI6MTYzNzc2OTYyOX0.YjPQnzweBnzwVaRfo6QMfLzbvyf3xX2KJR7CtOJkBMYdiDRQA27Gj2I_pR0GXGl_Id87y5teGwUNoZDk60BnNw",
	"username": "user1"
    }
}

4.1.2. curl

curl --basic -u user1:kissa13 $host/v1/login | jq '.'
{
  "message": "Basic auth succeeded!",
  "user": {
    "username": "user1",
    "id": 1,
    "roles": [
      "admin",
      "user"
    ],
    "token": "eyJhbGciOiJIUzUxMiJ9.eyJpZCI6MSwicm9sZXMiOlsiYWRtaW4iLCJ1c2VyIl0sImV4cCI6MTYzODg5NDAyM30.9E1Wfyc_3iNn1IPnvrnU5qUV9Y4UnwE5UbG9uVRudrqq6gK4qbaS_425qOF-bBHZum0YvasDdUD9rbjdN9ZF5g"
  }
}

4.2. Tenant

http --ignore-stdin $host/v1/tenants \
     Authorization:"Token ${auth_token}" \
     name="tenant 1"
{"id":118,"name":"tenant 1"}

4.3. Roster

To create a Roster with the shifts planned, simply POST a Roster where the shifts are not yet planned. Optionally, if you want to send a partially planned Roster, instead, you can.

Find the schema definition of Roster in the appendix or directly in the Swagger API documentation: https://compute-demo.200ok.ch/index.html#/roster/post_v1_tenants__tenant_id__compute_roster.

Here's an example Roster with no planned shifts:

{
  "meta": {
    "ruleFilePaths": [
      "rosterConstraints.drl"
    ],
    "secondsSpentLimit": 10
  },
  "skills": [
    {
      "id": "reiskochen",
      "name": "Reis kochen"
    },
    {
      "id": "reiswaschen",
      "name": "Reis waschen"
    }
  ],
  "spots": [
    {
      "id": "reiskocher",
      "name": "Reiskocher",
      "requiredSkills": [
	"reiskochen"
      ]
    },
    {
      "id": "lavabo",
      "name": "Lavabo",
      "requiredSkills": [
	"reiswaschen"
      ]
    }
  ],
  "contracts": [
    {
      "id": "fulltime",
      "name": "Fulltime",
      "maximumMinutesPerDay": 0,
      "maximumMinutesPerWeek": 2550,
      "maximumMinutesPerMonth": 0,
      "maximumMinutesPerYear": 0
    }
  ],
  "employees": [
    {
      "id": "mrsrice",
      "name": "Mrs Rice",
      "contract": "fulltime",
      "skills": [
	"reiskochen",
	"reiswaschen"
      ]
    },
    {
      "id": "mrrice",
      "name": "Mr Rice",
      "contract": "fulltime",
      "skills": [
	"reiswaschen"
      ]
    }
  ],
  "shifts": [
    {
      "id": "82c397c6-8d58-4329-9c94-69d3637b6b1b",
      "spot": "reiskocher",
      "startDateTime": "2021-06-23T09:00",
      "endDateTime": "2021-06-23T10:00"
    },
    {
      "id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7",
      "spot": "reiskocher",
      "startDateTime": "2021-06-23T09:30",
      "endDateTime": "2021-06-23T10:30"
    },
    {
      "id": "3e1fec47-003e-4d9f-a0b7-4d49405b2dee",
      "spot": "lavabo",
      "startDateTime": "2021-06-23T08:30",
      "endDateTime": "2021-06-23T09:00"
    }
  ]
}

4.3.1. Create Roster

To optimize a Roster, you create a Job of a specific type. The type depends on what resource you want to optimize for. Let's first look at the case of a classic shift operation. In a classic shift operation, during optimization, shifts are assigned to employees. Hence, the Job type is called optimize_for_employees.

NB: For a complete list of available job types, please see job types.

Here is an example given the above example Roster.

echo $example_roster |  http $host/v1/tenants/$tenant_id/jobs \
			     Authorization:"Token ${auth_token}" \
			     webhook==$webhook_url \
			     type==optimize_for_employees
{"msg":"Job accepted.","webhook":"http://your-domain.ch/webhook","job_id":"cfc501ad-4b17-4c55-beed-4e2f5b59eb54"}

When the job is finished, the resulting Job will be POSTed to http://your-domain.ch/webhook. It will include some metadata. The actual Roster will be nested under the key results. The Roster, again, will include some metadata and planned shifts indicated by the employee id per Shift.

This is what it looks like:

{
  "payload": {
    "meta": {
      "ruleFilePaths": [
	"rosterConstraints.drl"
      ],
      "secondsSpentLimit": 10
    },
    "spots": [
      {
	"id": "reiskocher",
	"name": "Reiskocher",
	"requiredSkills": [
	  "reiskochen"
	]
      },
      {
	"id": "lavabo",
	"name": "Lavabo",
	"requiredSkills": [
	  "reiswaschen"
	]
      }
    ],
    "shifts": [
      {
	"id": "82c397c6-8d58-4329-9c94-69d3637b6b1b",
	"spot": "reiskocher",
	"endDateTime": "2021-06-23T10:00",
	"startDateTime": "2021-06-23T09:00"
      },
      {
	"id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7",
	"spot": "reiskocher",
	"endDateTime": "2021-06-23T10:30",
	"startDateTime": "2021-06-23T09:30"
      },
      {
	"id": "3e1fec47-003e-4d9f-a0b7-4d49405b2dee",
	"spot": "lavabo",
	"endDateTime": "2021-06-23T09:00",
	"startDateTime": "2021-06-23T08:30"
      }
    ],
    "skills": [
      {
	"id": "reiskochen",
	"name": "Reis kochen"
      },
      {
	"id": "reiswaschen",
	"name": "Reis waschen"
      }
    ],
    "contracts": [
      {
	"id": "fulltime",
	"name": "Fulltime",
	"maximumMinutesPerDay": 0,
	"maximumMinutesPerWeek": 2550,
	"maximumMinutesPerYear": 0,
	"maximumMinutesPerMonth": 0
      }
    ],
    "employees": [
      {
	"id": "mrsrice",
	"name": "Mrs Rice",
	"skills": [
	  "reiskochen",
	  "reiswaschen"
	],
	"contract": "fulltime"
      },
      {
	"id": "mrrice",
	"name": "Mr Rice",
	"skills": [
	  "reiswaschen"
	],
	"contract": "fulltime"
      }
    ]
  },
  "started_at": "2022-01-25T16:31:12.762Z",
  "completed_at": "2022-01-25T16:31:25.287Z",
  "webhook": "https://webhook.site/e67d9837-5ae0-468f-8e04-04bf34bc87ea",
  "indictment": {
    "charge-0": {
      "justification": {
	"id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7",
	"spot": {
	  "id": "reiskocher",
	  "name": "Reiskocher",
	  "maximumMinutes": 0,
	  "requiredSkills": [
	    {
	      "id": "reiskochen",
	      "name": "Reis kochen"
	    }
	  ]
	},
	"startDateTime": "2021-06-23T09:30",
	"endDateTime": "2021-06-23T10:30",
	"requiredSkills": null,
	"employee": {
	  "id": "mrrice",
	  "name": "Mr Rice",
	  "contract": {
	    "id": "fulltime",
	    "name": "Fulltime",
	    "maximumMinutesPerDay": 0,
	    "maximumMinutesPerWeek": 2550,
	    "maximumMinutesPerMonth": 0,
	    "maximumMinutesPerYear": 0
	  },
	  "skills": [
	    {
	      "id": "reiswaschen",
	      "name": "Reis waschen"
	    }
	  ]
	},
	"lockedEmployee": null
      },
      "constraintMatchSet": [
	{
	  "constraintName": "employeeLacksRequiredSkillForSpot",
	  "justificationList": [
	    {
	      "id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7",
	      "spot": {
		"id": "reiskocher",
		"name": "Reiskocher",
		"maximumMinutes": 0,
		"requiredSkills": [
		  {
		    "id": "reiskochen",
		    "name": "Reis kochen"
		  }
		]
	      },
	      "startDateTime": "2021-06-23T09:30",
	      "endDateTime": "2021-06-23T10:30",
	      "requiredSkills": null,
	      "employee": {
		"id": "mrrice",
		"name": "Mr Rice",
		"contract": {
		  "id": "fulltime",
		  "name": "Fulltime",
		  "maximumMinutesPerDay": 0,
		  "maximumMinutesPerWeek": 2550,
		  "maximumMinutesPerMonth": 0,
		  "maximumMinutesPerYear": 0
		},
		"skills": [
		  {
		    "id": "reiswaschen",
		    "name": "Reis waschen"
		  }
		]
	      },
	      "lockedEmployee": null
	    }
	  ],
	  "score": {
	    "initScore": 0,
	    "hardScore": -60,
	    "softScore": 0,
	    "feasible": false,
	    "solutionInitialized": true
	  },
	  "constraintId": "defaultpkg/employeeLacksRequiredSkillForSpot"
	}
      ],
      "score": {
	"initScore": 0,
	"hardScore": -60,
	"softScore": 0,
	"feasible": false,
	"solutionInitialized": true
      },
      "constraintMatchCount": 1
    }
  },
  "type": "optimize_for_employees",
  "state": "complete",
  "id": 147,
  "tenant_id": 1,
  "uuid": "bcdc1f19-c788-4379-bd6f-d03f4f87c702",
  "created_at": "2022-01-25T16:31:12.651Z",
  "results": [
    {
      "id": 209,
      "job_id": 147,
      "payload": {
	"meta": {
	  "ruleFilePaths": [
	    "rosterConstraints.drl"
	  ],
	  "secondsSpentLimit": 10
	},
	"score": {
	  "feasible": false,
	  "hardScore": -60,
	  "initScore": 0,
	  "softScore": 0,
	  "solutionInitialized": true
	},
	"spots": [
	  {
	    "id": "reiskocher",
	    "name": "Reiskocher",
	    "maximumMinutes": 0,
	    "requiredSkills": [
	      "reiskochen"
	    ]
	  },
	  {
	    "id": "lavabo",
	    "name": "Lavabo",
	    "maximumMinutes": 0,
	    "requiredSkills": [
	      "reiswaschen"
	    ]
	  }
	],
	"shifts": [
	  {
	    "id": "82c397c6-8d58-4329-9c94-69d3637b6b1b",
	    "spot": "reiskocher",
	    "employee": "mrsrice",
	    "endDateTime": "2021-06-23T10:00",
	    "startDateTime": "2021-06-23T09:00",
	    "lockedEmployee": null,
	    "requiredSkills": null
	  },
	  {
	    "id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7",
	    "spot": "reiskocher",
	    "employee": "mrrice",
	    "endDateTime": "2021-06-23T10:30",
	    "startDateTime": "2021-06-23T09:30",
	    "lockedEmployee": null,
	    "requiredSkills": null
	  },
	  {
	    "id": "3e1fec47-003e-4d9f-a0b7-4d49405b2dee",
	    "spot": "lavabo",
	    "employee": "mrsrice",
	    "endDateTime": "2021-06-23T09:00",
	    "startDateTime": "2021-06-23T08:30",
	    "lockedEmployee": null,
	    "requiredSkills": null
	  }
	],
	"skills": [
	  {
	    "id": "reiskochen",
	    "name": "Reis kochen"
	  },
	  {
	    "id": "reiswaschen",
	    "name": "Reis waschen"
	  }
	],
	"contracts": [
	  {
	    "id": "fulltime",
	    "name": "Fulltime",
	    "maximumMinutesPerDay": 0,
	    "maximumMinutesPerWeek": 2550,
	    "maximumMinutesPerYear": 0,
	    "maximumMinutesPerMonth": 0
	  }
	],
	"employees": [
	  {
	    "id": "mrsrice",
	    "name": "Mrs Rice",
	    "skills": [
	      "reiskochen",
	      "reiswaschen"
	    ],
	    "contract": "fulltime"
	  },
	  {
	    "id": "mrrice",
	    "name": "Mr Rice",
	    "skills": [
	      "reiswaschen"
	    ],
	    "contract": "fulltime"
	  }
	],
	"availabilities": []
      },
      "final": true,
      "created_at": "2022-01-25T16:31:25.276Z"
    }
  ]
}

4.3.2. Validate Roster

To validate a Roster against the given rules, you create a Job of type score_roster. Here is an example given the above example Roster.

echo $example_roster |  http $host/v1/tenants/$tenant_id/jobs/ \
			     Authorization:"Token ${auth_token}" \
			     webhook==$webhook_url \
			     type==score_roster
{"msg":"Job accepted.","webhook":"https://webhook.site/3d45188b-2932-4f37-8115-b65cfaadba26","job_id":"dfe1209d-7fad-4582-9e6c-b7551a037542"}

This is what the payload will look like. It includes the key indictment which has all the information regarding the given rules. Each of them has various metadata including a score. The top level also includes the key results which includes your given Roster, but has the overall score updated.

{
  "payload": {
    "meta": {
      "ruleFilePaths": [
	"rosterConstraints.drl"
      ],
      "secondsSpentLimit": 10
    },
    "spots": [
      {
	"id": "reiskocher",
	"name": "Reiskocher",
	"requiredSkills": [
	  "reiskochen"
	]
      },
      {
	"id": "lavabo",
	"name": "Lavabo",
	"requiredSkills": [
	  "reiswaschen"
	]
      }
    ],
    "shifts": [
      {
	"id": "82c397c6-8d58-4329-9c94-69d3637b6b1b",
	"spot": "reiskocher",
	"endDateTime": "2021-06-23T10:00",
	"startDateTime": "2021-06-23T09:00"
      },
      {
	"id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7",
	"spot": "reiskocher",
	"endDateTime": "2021-06-23T10:30",
	"startDateTime": "2021-06-23T09:30"
      },
      {
	"id": "3e1fec47-003e-4d9f-a0b7-4d49405b2dee",
	"spot": "lavabo",
	"endDateTime": "2021-06-23T09:00",
	"startDateTime": "2021-06-23T08:30"
      }
    ],
    "skills": [
      {
	"id": "reiskochen",
	"name": "Reis kochen"
      },
      {
	"id": "reiswaschen",
	"name": "Reis waschen"
      }
    ],
    "contracts": [
      {
	"id": "fulltime",
	"name": "Fulltime",
	"maximumMinutesPerDay": 0,
	"maximumMinutesPerWeek": 2550,
	"maximumMinutesPerYear": 0,
	"maximumMinutesPerMonth": 0
      }
    ],
    "employees": [
      {
	"id": "mrsrice",
	"name": "Mrs Rice",
	"skills": [
	  "reiskochen",
	  "reiswaschen"
	],
	"contract": "fulltime"
      },
      {
	"id": "mrrice",
	"name": "Mr Rice",
	"skills": [
	  "reiswaschen"
	],
	"contract": "fulltime"
      }
    ]
  },
  "started_at": "2021-12-02T15:56:25.367Z",
  "completed_at": "2021-12-02T15:56:28.047Z",
  "webhook": "https://webhook.site/3d45188b-2932-4f37-8115-b65cfaadba26",
  "indictment": {
    "charge-0": {
      "justification": {
	"id": "82c397c6-8d58-4329-9c94-69d3637b6b1b",
	"spot": {
	  "id": "reiskocher",
	  "name": "Reiskocher",
	  "requiredSkills": [
	    {
	      "id": "reiskochen",
	      "name": "Reis kochen"
	    }
	  ]
	},
	"startDateTime": "2021-06-23T09:00",
	"endDateTime": "2021-06-23T10:00",
	"requiredSkills": null,
	"employee": null,
	"lockedEmployee": null
      },
      "constraintMatchSet": [
	{
	  "constraintName": "shiftRemainsUnassigned",
	  "justificationList": [
	    {
	      "id": "82c397c6-8d58-4329-9c94-69d3637b6b1b",
	      "spot": {
		"id": "reiskocher",
		"name": "Reiskocher",
		"requiredSkills": [
		  {
		    "id": "reiskochen",
		    "name": "Reis kochen"
		  }
		]
	      },
	      "startDateTime": "2021-06-23T09:00",
	      "endDateTime": "2021-06-23T10:00",
	      "requiredSkills": null,
	      "employee": null,
	      "lockedEmployee": null
	    }
	  ],
	  "score": {
	    "initScore": 0,
	    "hardScore": -60,
	    "softScore": 0,
	    "feasible": false,
	    "solutionInitialized": true
	  },
	  "constraintId": "defaultpkg/shiftRemainsUnassigned"
	}
      ],
      "score": {
	"initScore": 0,
	"hardScore": -60,
	"softScore": 0,
	"feasible": false,
	"solutionInitialized": true
      },
      "constraintMatchCount": 1
    },
    "charge-1": {
      "justification": {
	"id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7",
	"spot": {
	  "id": "reiskocher",
	  "name": "Reiskocher",
	  "requiredSkills": [
	    {
	      "id": "reiskochen",
	      "name": "Reis kochen"
	    }
	  ]
	},
	"startDateTime": "2021-06-23T09:30",
	"endDateTime": "2021-06-23T10:30",
	"requiredSkills": null,
	"employee": null,
	"lockedEmployee": null
      },
      "constraintMatchSet": [
	{
	  "constraintName": "shiftRemainsUnassigned",
	  "justificationList": [
	    {
	      "id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7",
	      "spot": {
		"id": "reiskocher",
		"name": "Reiskocher",
		"requiredSkills": [
		  {
		    "id": "reiskochen",
		    "name": "Reis kochen"
		  }
		]
	      },
	      "startDateTime": "2021-06-23T09:30",
	      "endDateTime": "2021-06-23T10:30",
	      "requiredSkills": null,
	      "employee": null,
	      "lockedEmployee": null
	    }
	  ],
	  "score": {
	    "initScore": 0,
	    "hardScore": -60,
	    "softScore": 0,
	    "feasible": false,
	    "solutionInitialized": true
	  },
	  "constraintId": "defaultpkg/shiftRemainsUnassigned"
	}
      ],
      "score": {
	"initScore": 0,
	"hardScore": -60,
	"softScore": 0,
	"feasible": false,
	"solutionInitialized": true
      },
      "constraintMatchCount": 1
    },
    "charge-2": {
      "justification": {
	"id": "3e1fec47-003e-4d9f-a0b7-4d49405b2dee",
	"spot": {
	  "id": "lavabo",
	  "name": "Lavabo",
	  "requiredSkills": [
	    {
	      "id": "reiswaschen",
	      "name": "Reis waschen"
	    }
	  ]
	},
	"startDateTime": "2021-06-23T08:30",
	"endDateTime": "2021-06-23T09:00",
	"requiredSkills": null,
	"employee": null,
	"lockedEmployee": null
      },
      "constraintMatchSet": [
	{
	  "constraintName": "shiftRemainsUnassigned",
	  "justificationList": [
	    {
	      "id": "3e1fec47-003e-4d9f-a0b7-4d49405b2dee",
	      "spot": {
		"id": "lavabo",
		"name": "Lavabo",
		"requiredSkills": [
		  {
		    "id": "reiswaschen",
		    "name": "Reis waschen"
		  }
		]
	      },
	      "startDateTime": "2021-06-23T08:30",
	      "endDateTime": "2021-06-23T09:00",
	      "requiredSkills": null,
	      "employee": null,
	      "lockedEmployee": null
	    }
	  ],
	  "score": {
	    "initScore": 0,
	    "hardScore": -30,
	    "softScore": 0,
	    "feasible": false,
	    "solutionInitialized": true
	  },
	  "constraintId": "defaultpkg/shiftRemainsUnassigned"
	}
      ],
      "score": {
	"initScore": 0,
	"hardScore": -30,
	"softScore": 0,
	"feasible": false,
	"solutionInitialized": true
      },
      "constraintMatchCount": 1
    }
  },
  "type": "score_roster",
  "state": "complete",
  "id": 92,
  "tenant_id": 179,
  "uuid": "dfe1209d-7fad-4582-9e6c-b7551a037542",
  "created_at": "2021-12-02T15:56:25.310Z",
  "results": [
    {
      "id": 107,
      "job_id": 92,
      "payload": {
	"meta": {
	  "ruleFilePaths": [
	    "rosterConstraints.drl"
	  ],
	  "secondsSpentLimit": 10
	},
	"score": {
	  "feasible": false,
	  "hardScore": -150,
	  "initScore": -3,
	  "softScore": 0,
	  "solutionInitialized": false
	},
	"spots": [
	  {
	    "id": "reiskocher",
	    "name": "Reiskocher",
	    "requiredSkills": [
	      "reiskochen"
	    ]
	  },
	  {
	    "id": "lavabo",
	    "name": "Lavabo",
	    "requiredSkills": [
	      "reiswaschen"
	    ]
	  }
	],
	"shifts": [
	  {
	    "id": "82c397c6-8d58-4329-9c94-69d3637b6b1b",
	    "spot": "reiskocher",
	    "employee": null,
	    "endDateTime": "2021-06-23T10:00",
	    "startDateTime": "2021-06-23T09:00",
	    "lockedEmployee": null,
	    "requiredSkills": null
	  },
	  {
	    "id": "bb6ac335-e67b-43b3-8867-f8ef3e90bfb7",
	    "spot": "reiskocher",
	    "employee": null,
	    "endDateTime": "2021-06-23T10:30",
	    "startDateTime": "2021-06-23T09:30",
	    "lockedEmployee": null,
	    "requiredSkills": null
	  },
	  {
	    "id": "3e1fec47-003e-4d9f-a0b7-4d49405b2dee",
	    "spot": "lavabo",
	    "employee": null,
	    "endDateTime": "2021-06-23T09:00",
	    "startDateTime": "2021-06-23T08:30",
	    "lockedEmployee": null,
	    "requiredSkills": null
	  }
	],
	"skills": [
	  {
	    "id": "reiskochen",
	    "name": "Reis kochen"
	  },
	  {
	    "id": "reiswaschen",
	    "name": "Reis waschen"
	  }
	],
	"contracts": [
	  {
	    "id": "fulltime",
	    "name": "Fulltime",
	    "maximumMinutesPerDay": 0,
	    "maximumMinutesPerWeek": 2550,
	    "maximumMinutesPerYear": 0,
	    "maximumMinutesPerMonth": 0
	  }
	],
	"employees": [
	  {
	    "id": "mrsrice",
	    "name": "Mrs Rice",
	    "skills": [
	      "reiskochen",
	      "reiswaschen"
	    ],
	    "contract": "fulltime"
	  },
	  {
	    "id": "mrrice",
	    "name": "Mr Rice",
	    "skills": [
	      "reiswaschen"
	    ],
	    "contract": "fulltime"
	  }
	],
	"availabilities": []
      },
      "final": true,
      "created_at": "2021-12-02T15:56:28.035Z"
    }
  ]
}

4.4. Get Job (Polling)

echo $compute_roster | jq .job_id
http --ignore-stdin --pretty format \
     $host/v1/tenants/$tenant_id/jobs/$job_id \
     Authorization:"Token ${auth_token}"
{
    "coercion": "spec",
    "in": [
	"request",
	"path-params"
    ],
    "problems": [
	{
	    "in": [
		"tenant_id"
	    ],
	    "path": [
		"tenant_id"
	    ],
	    "pred": "clojure.core/int?",
	    "val": "nil",
	    "via": [
		"spec$220/tenant_id"
	    ]
	}
    ],
    "spec": "(spec-tools.core/spec {:spec (clojure.spec.alpha/keys :req-un [:spec$220/tenant_id :spec$220/uuid]), :type :map, :leaf? false})",
    "type": "reitit.coercion/request-coercion",
    "value": {
	"tenant_id": "nil",
	"uuid": "nil"
    }
}

5. Appendix

5.1. Schema defintions

5.1.1. Roster

  1. Spec
    (s/def ::secondsSpentLimit int?)
    
    (s/def ::ruleFilePath string?)
    
    (s/def ::ruleFilePaths (s/coll-of ::ruleFilePath))
    
    (s/def ::meta (s/keys :req-un [::ruleFilePaths
    			       ::secondsSpentLimit]))
    
    (def ^:private id?
      (partial re-matches #"[\w-:.,;/]+"))
    
    (s/def ::id (s/and string? id?))
    
    (s/def ::name string?)
    
    (s/def ::maximumMinutesPerDayPerEmployee int?)
    
    (s/def ::maximumMinutesPerWeekPerEmployee int?)
    
    (s/def ::maximumMinutesPerMonthPerEmployee int?)
    
    (s/def ::maximumMinutesPerYearPerEmployee int?)
    
    (s/def ::skill (s/keys :req-un [::id
    				::name]
    		       :opt-un [::maximumMinutesPerWeekPerEmployee]))
    
    (s/def ::skills (s/coll-of ::skill))
    
    (s/def ::requiredSkills (s/coll-of ::id))
    
    (s/def ::maximumMinutes int?)
    
    (s/def ::spot (s/keys :req-un [::id
    			       ::name]
    		      :opt-un [::requiredSkills
    			       ::maximumMinutes]))
    
    (s/def ::spots (s/coll-of ::spot))
    
    (s/def ::maximumMinutesPerDay int?)
    
    (s/def ::maximumMinutesPerWeek int?)
    
    (s/def ::maximumMinutesPerMonth int?)
    
    (s/def ::maximumMinutesPerYear int?)
    
    (s/def ::contract (s/keys :req-un [::id
    				   ::name]
    			  :opt-un [::maximumMinutesPerDay
    				   ::maximumMinutesPerWeek
    				   ::maximumMinutesPerMonth
    				   ::maximumMinutesPerYear]))
    
    (s/def ::contracts (s/coll-of ::contract))
    
    (s/def :id/contract ::id)
    (s/def :id/skills (s/coll-of ::id))
    
    (s/def ::employee (s/keys :req-un [::id
    				   ::name
    				   :id/contract
    				   :id/skills]))
    
    (s/def ::employees (s/coll-of ::employee))
    
    (s/def :id/spot ::id)
    
    (s/def :id/employee ::id)
    
    (s/def ::lockedEmployee ::id)
    
    (s/def ::shift (s/or
    		:optimize-for-employee
    		(s/keys :req-un [::id
    				 :id/spot
    				 ::startDateTime
    				 ::endDateTime]
    			:opt-un [::requiredSkills
    				 :id/employee
    				 ::lockedEmployee])
    		:optimize-for-spot
    		(s/keys :req-un [::id
    				 :id/employee
    				 ::startDateTime
    				 ::endDateTime]
    			:opt-un [::requiredSkills
    				 :id/spot
    				 ::lockedEmployee])))
    
    (s/def ::shifts (s/coll-of ::shift))
    
    (def ^:private datetime?
      (partial re-matches #"\d{4}-\d\d-\d\dT\d\d:\d\d"))
    
    (s/def ::startDateTime (s/and string? datetime?))
    
    (s/def ::endDateTime (s/and string? datetime?))
    
    (s/def ::state #{"UNAVAILABLE"
    		 "UNDESIRED"
    		 "DESIRED"})
    
    (s/def ::availability (s/keys :req-un [::startDateTime
    				       ::endDateTime
    				       ::state
    				       :id/employee]))
    
    (s/def ::availabilities (s/coll-of ::availability))
    
    (s/def ::initScore int?)
    
    (s/def ::hardScore int?)
    
    (s/def ::softScore int?)
    
    (s/def ::feasible boolean?)
    
    (s/def ::solutionInitialized boolean?)
    
    (s/def ::score (s/keys :req-un [::initScore
    				::hardScore
    				::softScore
    				::feasible
    				::solutionInitialized]))
    
    (s/def ::roster (s/keys :req-un [::skills
    				 ::spots
    				 ::contracts
    				 ::employees
    				 ::shifts]
    
    			:opt-un [::meta
    				 ::availabilities
    				 ::score]))
    
  2. Swagger
    {
      "type": "object",
      "properties": {
        "skills": {
          "type": "array",
          "items": {
    	"type": "object",
    	"properties": {
    	  "id": {
    	    "type": "string",
    	    "x-allOf": [
    	      {
    		"type": "string"
    	      },
    	      {}
    	    ]
    	  },
    	  "name": {
    	    "type": "string"
    	  }
    	},
    	"required": [
    	  "id",
    	  "name"
    	],
    	"title": "spapi.roster/skill"
          }
        },
        "spots": {
          "type": "array",
          "items": {
    	"type": "object",
    	"properties": {
    	  "id": {
    	    "type": "string",
    	    "x-allOf": [
    	      {
    		"type": "string"
    	      },
    	      {}
    	    ]
    	  },
    	  "name": {
    	    "type": "string"
    	  },
    	  "requiredSkills": {
    	    "type": "array",
    	    "items": {
    	      "type": "string",
    	      "x-allOf": [
    		{
    		  "type": "string"
    		},
    		{}
    	      ]
    	    }
    	  }
    	},
    	"required": [
    	  "id",
    	  "name"
    	],
    	"title": "spapi.roster/spot"
          }
        },
        "contracts": {
          "type": "array",
          "items": {
    	"type": "object",
    	"properties": {
    	  "id": {
    	    "type": "string",
    	    "x-allOf": [
    	      {
    		"type": "string"
    	      },
    	      {}
    	    ]
    	  },
    	  "name": {
    	    "type": "string"
    	  },
    	  "maximumMinutesPerDay": {
    	    "type": "integer",
    	    "format": "int64"
    	  },
    	  "maximumMinutesPerWeek": {
    	    "type": "integer",
    	    "format": "int64"
    	  },
    	  "maximumMinutesPerMonth": {
    	    "type": "integer",
    	    "format": "int64"
    	  },
    	  "maximumMinutesPerYear": {
    	    "type": "integer",
    	    "format": "int64"
    	  }
    	},
    	"required": [
    	  "id",
    	  "name"
    	],
    	"title": "spapi.roster/contract"
          }
        },
        "employees": {
          "type": "array",
          "items": {
    	"type": "object",
    	"properties": {
    	  "id": {
    	    "type": "string",
    	    "x-allOf": [
    	      {
    		"type": "string"
    	      },
    	      {}
    	    ]
    	  },
    	  "name": {
    	    "type": "string"
    	  },
    	  "contract": {
    	    "type": "string",
    	    "x-allOf": [
    	      {
    		"type": "string"
    	      },
    	      {}
    	    ]
    	  },
    	  "skills": {
    	    "type": "array",
    	    "items": {
    	      "type": "string",
    	      "x-allOf": [
    		{
    		  "type": "string"
    		},
    		{}
    	      ]
    	    }
    	  }
    	},
    	"required": [
    	  "id",
    	  "name",
    	  "contract",
    	  "skills"
    	],
    	"title": "spapi.roster/employee"
          }
        },
        "shifts": {
          "type": "array",
          "items": {
    	"type": "object",
    	"properties": {
    	  "spot": {
    	    "type": "string",
    	    "x-allOf": [
    	      {
    		"type": "string"
    	      },
    	      {}
    	    ]
    	  },
    	  "startDateTime": {
    	    "type": "string",
    	    "x-allOf": [
    	      {
    		"type": "string"
    	      },
    	      {}
    	    ]
    	  },
    	  "endDateTime": {
    	    "type": "string",
    	    "x-allOf": [
    	      {
    		"type": "string"
    	      },
    	      {}
    	    ]
    	  },
    	  "requiredSkills": {
    	    "type": "array",
    	    "items": {
    	      "type": "string",
    	      "x-allOf": [
    		{
    		  "type": "string"
    		},
    		{}
    	      ]
    	    }
    	  },
    	  "employee": {
    	    "type": "string",
    	    "x-allOf": [
    	      {
    		"type": "string"
    	      },
    	      {}
    	    ]
    	  },
    	  "lockedEmployee": {
    	    "type": "string",
    	    "x-allOf": [
    	      {
    		"type": "string"
    	      },
    	      {}
    	    ]
    	  }
    	},
    	"required": [
    	  "spot",
    	  "startDateTime",
    	  "endDateTime"
    	],
    	"title": "spapi.roster/shift"
          }
        },
        "meta": {
          "type": "object",
          "properties": {
    	"ruleFilePaths": {
    	  "type": "array",
    	  "items": {
    	    "type": "string"
    	  }
    	},
    	"secondsSpentLimit": {
    	  "type": "integer",
    	  "format": "int64"
    	}
          },
          "required": [
    	"ruleFilePaths",
    	"secondsSpentLimit"
          ],
          "title": "spapi.roster/meta"
        },
        "availabilities": {
          "type": "array",
          "items": {
    	"type": "object",
    	"properties": {
    	  "startDateTime": {
    	    "type": "string",
    	    "x-allOf": [
    	      {
    		"type": "string"
    	      },
    	      {}
    	    ]
    	  },
    	  "endDateTime": {
    	    "type": "string",
    	    "x-allOf": [
    	      {
    		"type": "string"
    	      },
    	      {}
    	    ]
    	  },
    	  "state": {
    	    "enum": [
    	      "DESIRED",
    	      "UNAVAILABLE",
    	      "UNDESIRED"
    	    ],
    	    "type": "string"
    	  },
    	  "employee": {
    	    "type": "string",
    	    "x-allOf": [
    	      {
    		"type": "string"
    	      },
    	      {}
    	    ]
    	  }
    	},
    	"required": [
    	  "startDateTime",
    	  "endDateTime",
    	  "state",
    	  "employee"
    	],
    	"title": "spapi.roster/availability"
          }
        },
        "score": {
          "type": "object",
          "properties": {
    	"initScore": {
    	  "type": "integer",
    	  "format": "int64"
    	},
    	"hardScore": {
    	  "type": "integer",
    	  "format": "int64"
    	},
    	"softScore": {
    	  "type": "integer",
    	  "format": "int64"
    	},
    	"feasible": {
    	  "type": "boolean"
    	},
    	"solutionInitialized": {
    	  "type": "boolean"
    	}
          },
          "required": [
    	"initScore",
    	"hardScore",
    	"softScore",
    	"feasible",
    	"solutionInitialized"
          ],
          "title": "spapi.roster/score"
        }
      },
      "required": [
        "skills",
        "spots",
        "contracts",
        "employees",
        "shifts"
      ],
      "title": "spapi.roster/roster"
    }
    

Author: 200ok GmbH

Created: 2022-11-28 Mon 10:41

Validate