Skip to main content
A skill is a label for something a resource can do or carries. A gas certification and a refrigerated body are two examples. You decide the labels, and the engine only compares them as text. The order carries the requirement, in requiredSkills. The resource carries what it has, in skills. The engine gives the order only to a resource whose skills hold every skill the order requires. This guide covers requiredSkills and skills, the softer preferredStopTags, and the forbiddenAssignment constraint. Each section states one rule, with a link to the API reference where the exact behavior is written. The last section shows how to check a solution against these rules.

Reserve an order for qualified resources

A boiler repair needs a technician with a gas certification. Write the requirement on the order, and give the certification to the technicians who hold it:
The engine gives the boiler order to tech-certified, even when tech-plain lives next to the customer. In the sandbox, a certified technician 7 km away served the job, and the uncertified technician next door did not. When no resource qualifies, the engine doesn’t fail. It leaves the stop out of every tour and lists it in unaffectedStopIds. The plan is still optimized, and the solution reports no violation. You need to read unaffectedStopIds to see the gap. See Checking the solution. See Order.requiredSkills and Resource.skills for the rule.

Vehicles with equipment

The equipment of a vehicle works like the certification of a person. Say that only one van has a refrigerated body. Give that van the skill, and require the skill on the orders that need cold:
The fresh order goes to van-refrigerated, even when van-ambient starts closer. In the sandbox, the engine sent the fresh order to the refrigerated van although the ambient van was next to it. A requirement doesn’t reserve the resource. An order without requiredSkills can go to any resource, including one that has skills. In the same sandbox test, the refrigerated van also took the dry order, and the ambient van stayed unused. The default objectives try to use as few resources as possible before they shorten the distance.

How skills match

  • Every skill is needed. An order that requires gas-certified and electrical-certified goes only to a resource that has both. A resource with only one of them can’t serve it.
  • The match is exact. The text must be identical, case included. A resource with Gas-Certified doesn’t serve an order that requires gas-certified. Choose one naming habit, such as lowercase words joined by hyphens, so that every system that writes a plan writes the same skill the same way.
  • Tags are not skills. A string in the tags of a resource doesn’t satisfy requiredSkills. Put it in skills.
  • The requirement belongs to the order. requiredSkills is a field of the order, not of the stop. If you write it on a stop, the API drops it and restricts nothing. Both SDKs refuse it before they send the plan. With kardinal-ai 1.1.0 and @kardinal-ai/sdk 1.1.0, the error names the field plan.orders[0].stops[0].requiredSkills.

Prefer instead of require

Sometimes a resource should handle a stop when it can, but another resource may cover for it. A senior technician should take a demanding job, and a generalist can still do it. For this, tag the stop and use preferredStopTags on the resource, instead of requiredSkills:
The senior technician takes the tagged stop even when a generalist lives next to it. When the senior technician can’t serve it, for example because the working window is too short, another resource takes the stop. The stop stays planned. The default list of objectives already holds maximizePreferredStops, after minimizeCosts and minimizeResources. In the sandbox, with the default list, the senior technician took the preferred stop and the plain one together, because one resource serves both. If you write your own objectives list, add maximizePreferredStops yourself. See Resource.preferredStopTags for the rule.

Barring one resource from a stop

forbiddenAssignment bars one resource tag from one stop tag. It fits when you know one resource that must stay away from one stop, and that resource carries the tag. For example, technician 1 is banned from a site, so the technician carries technician:1 and the stop carries site:restricted:
The engine never gives the stop to a resource that carries technician:1. It gives the stop to another resource. When no other resource can serve it, the stop stays unplanned, with no violation. The rule bars only the resources that carry the tag. A technician that you add later without the tag can serve the site, and the engine doesn’t warn you. See the Modelling pitfall of forbiddenAssignment in the API reference. For a rule about a kind of resource, write a requirement instead of a ban. Take a city that forbids trucks over 7.5 t in its old town:
  • As a ban, you tag every heavy truck heavy-truck, tag the old-town stops, and add the forbiddenAssignment. A heavy truck added later without its tag can serve the old town. The mistake is silent.
  • As a requirement, you give small-vehicle to the vehicles allowed in the old town, and require it on the old-town orders. A vehicle added later without the skill can’t serve them. The mistake is visible: if no other vehicle has the skill, the stop stays in unaffectedStopIds until you add it.
A requirement fails visibly when a label is missing. A ban fails silently. Choose the requirement when the fleet changes.

Pinning a stop to one resource

To pin a stop to one resource, see the pattern “Isolate” in Manual assignment and supervision. A pinned stop is an exception to the rules of this guide. Say you pin a stop in state.assignments on a resource that lacks the skill. The engine keeps the stop there. The plan is still optimized, and no stop is unplanned. The tour of that resource has isValid: false and a violation of type skills, with the list lackingSkills. In the sandbox, this held for the three mode and status pairs tested. A stop pinned on a resource barred by a forbiddenAssignment stays too. The tour then has a violation of type forbiddenAssignment.

Checking the solution

The engine doesn’t report a missing skill as an error. Read these fields of the solution:
  • unaffectedStopIds lists the stops that no resource could serve, for example because none had the skill.
  • tours[].isValid is false when a hard rule is broken, for example a pinned stop on a resource without the skill.
  • tours[].violations says which rule. A violation of type skills lists the lackingSkills. A violation of type forbiddenAssignment lists the barred pair.

Example plan

This plan has two technicians. tech-certified holds the skill gas-certified, and tech-plain holds no skill. Each of the three orders shows one rule. The boiler needs a gas certification and sits next to tech-plain. The meter has no requirement. The panel needs an electrical certification that nobody holds.
The same plan with the SDKs:

What the solution shows

The sandbox returns this for the plan, with the offset of Europe/Paris and with seconds. The other fields are left out.
  • The boiler goes to tech-certified, the only technician with the skill, although tech-plain lives next to it.
  • The meter has no requirement. tech-certified serves it too, and tech-plain stays unused. The default objectives use as few resources as they can.
  • The panel is in unaffectedStopIds. Nobody holds electrical-certified, so no tour has it. The tour is valid and the plan is optimized: nothing in the solution calls this a failure.

A check in code

These snippets read the solution you got for the example plan. They list the unplanned stops. Then they print, for each tour, if it is valid and which skills it lacks. A violation of type forbiddenAssignment is printed too.
For the example plan, both print the same two lines: