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)

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 assigne 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

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-06-23 Thu 19:33

Validate