TL;DR

Two trucks with different equipment (crane, hazmat) and different per-dimension capacities (waste, recyclable). Jobs combine required_skills with multi-dimension demands; a skill-mismatched job is forced onto the correctly-equipped truck regardless of routing cost.

Skills & Capacities

required_skills on a job and skills on a resource form a hard eligibility filter, evaluated independently of and before the routing cost. demands and capacities can carry multiple independently-tracked named dimensions (e.g. separate waste and recyclable compartments) — a vehicle can serve a job only if every demand dimension fits within its remaining capacity for that dimension.

Setup

Two trucks at a shared depot, each with a different equipment profile and capacity split:

  • truck-crane-1: has skills: ["crane", "hazmat"], capacities: {"waste": 40, "recyclable": 20}.
  • truck-basic-2: has no special skills, capacities: {"waste": 60, "recyclable": 30} — more raw capacity, but cannot serve equipment-restricted jobs.

Five jobs:

  • job-crane-lift: requires required_skills: ["crane"] (a heavy item needing crane assistance). Demand {"waste": 8}.
  • job-hazmat-cleanup: requires required_skills: ["hazmat"]. Demand {"waste": 5}.
  • job-crane-hazmat: requires both required_skills: ["crane", "hazmat"]. Demand {"waste": 6, "recyclable": 4}.
  • job-routine-pickup: no skill requirement. Demand {"recyclable": 12}.
  • job-bulk-recycling: no skill requirement, but a large demand {"recyclable": 22} — only truck-basic-2 has enough remaining recyclable capacity once routing is accounted for.

Only truck-crane-1 has the crane and hazmat skills, so the first three jobs must all go to it regardless of routing cost — even though truck-basic-2 might be geographically closer to some of them. truck-basic-2 picks up the two unrestricted jobs.

Request

{
  "metadata": { "problem_id": "skills-capacities-example" },
  "travel": {
    "distance_matrix": [
      [0, 3100, 4800, 2600, 5200, 6100],
      [3100, 0, 2900, 4400, 3800, 5300],
      [4800, 2900, 0, 5600, 2100, 3400],
      [2600, 4400, 5600, 0, 6300, 7200],
      [5200, 3800, 2100, 6300, 0, 2700],
      [6100, 5300, 3400, 7200, 2700, 0]
    ]
  },
  "resources": [
    {
      "id": "truck-crane-1",
      "depot_index": 0,
      "capacities": { "waste": 40, "recyclable": 20 },
      "skills": ["crane", "hazmat"]
    },
    {
      "id": "truck-basic-2",
      "depot_index": 0,
      "capacities": { "waste": 60, "recyclable": 30 }
    }
  ],
  "jobs": [
    {
      "id": "job-crane-lift",
      "location_index": 1,
      "service_time": 1200,
      "demands": { "waste": 8 },
      "required_skills": ["crane"]
    },
    {
      "id": "job-hazmat-cleanup",
      "location_index": 2,
      "service_time": 900,
      "demands": { "waste": 5 },
      "required_skills": ["hazmat"]
    },
    {
      "id": "job-crane-hazmat",
      "location_index": 3,
      "service_time": 1500,
      "demands": { "waste": 6, "recyclable": 4 },
      "required_skills": ["crane", "hazmat"]
    },
    {
      "id": "job-routine-pickup",
      "location_index": 4,
      "service_time": 600,
      "demands": { "recyclable": 12 }
    },
    {
      "id": "job-bulk-recycling",
      "location_index": 5,
      "service_time": 900,
      "demands": { "recyclable": 22 }
    }
  ],
  "options": { "time_limit_seconds": 5 }
}

Response

{
  "status": "optimal",
  "problem_id": "skills-capacities-example",
  "solve_time_ms": 38,
  "routes": [
    {
      "resource_id": "truck-crane-1",
      "day": null,
      "activities": [
        { "type": "start", "location_index": 0, "departure_time": 0 },
        { "type": "service", "job_id": "job-crane-lift", "location_index": 1, "arrival_time": 3100, "departure_time": 4300, "service_time": 1200, "load_on_departure": { "total": 8, "by_dimension": { "waste": 8, "recyclable": 0 } }, "distance_from_prev": 3100, "time_from_prev": 3100 },
        { "type": "service", "job_id": "job-crane-hazmat", "location_index": 3, "arrival_time": 8700, "departure_time": 10200, "service_time": 1500, "load_on_departure": { "total": 18, "by_dimension": { "waste": 14, "recyclable": 4 } }, "distance_from_prev": 4400, "time_from_prev": 4400 },
        { "type": "service", "job_id": "job-hazmat-cleanup", "location_index": 2, "arrival_time": 15800, "departure_time": 16700, "service_time": 900, "load_on_departure": { "total": 23, "by_dimension": { "waste": 19, "recyclable": 4 } }, "distance_from_prev": 5600, "time_from_prev": 5600 },
        { "type": "end", "location_index": 0, "arrival_time": 21500, "distance_from_prev": 4800, "time_from_prev": 4800 }
      ],
      "summary": { "total_distance": 17900, "total_travel_time": 17900, "total_service_time": 3600, "total_waiting_time": 0, "total_time": 21500, "num_stops": 3, "num_jobs": 3, "max_loads": { "waste": 19, "recyclable": 4 }, "cost": 17900.0 },
      "violations": []
    },
    {
      "resource_id": "truck-basic-2",
      "day": null,
      "activities": [
        { "type": "start", "location_index": 0, "departure_time": 0 },
        { "type": "service", "job_id": "job-routine-pickup", "location_index": 4, "arrival_time": 5200, "departure_time": 5800, "service_time": 600, "load_on_departure": { "total": 12, "by_dimension": { "waste": 0, "recyclable": 12 } }, "distance_from_prev": 5200, "time_from_prev": 5200 },
        { "type": "service", "job_id": "job-bulk-recycling", "location_index": 5, "arrival_time": 8500, "departure_time": 9400, "service_time": 900, "load_on_departure": { "total": 34, "by_dimension": { "waste": 0, "recyclable": 34 } }, "distance_from_prev": 2700, "time_from_prev": 2700 },
        { "type": "end", "location_index": 0, "arrival_time": 15500, "distance_from_prev": 6100, "time_from_prev": 6100 }
      ],
      "summary": { "total_distance": 14000, "total_travel_time": 14000, "total_service_time": 1500, "total_waiting_time": 0, "total_time": 15500, "num_stops": 2, "num_jobs": 2, "max_loads": { "waste": 0, "recyclable": 34 }, "cost": 14000.0 },
      "violations": []
    }
  ],
  "unserved": [],
  "score": { "total": 31900.0, "travel_distance": 31900.0, "travel_time": 31900.0, "fixed_vehicle_cost": 0.0, "penalty_dropped": 0.0, "fairness_cost": 0.0, "num_vehicles_used": 2 },
  "warnings": []
}

Why the assignment is forced, not chosen

All three equipment-restricted jobs (job-crane-lift, job-hazmat-cleanup, job-crane-hazmat) land on truck-crane-1 because truck-basic-2 has no skills at all — it fails the required_skills filter on every one of them and is never an eligible candidate. This is a hard pre-filter applied before the solver evaluates routing cost at all, not a cost-driven choice; the solver never even considers sending job-hazmat-cleanup to truck-basic-2, even though geographically it could be cheaper.

load_on_departure.by_dimension shows each capacity dimension tracked independently: truck-crane-1's recyclable load only changes at job-crane-hazmat (the only job in its route with a recyclable demand), while its waste load accumulates across all three stops. truck-basic-2 never carries any waste at all — both of its jobs only have a recyclable demand, and its higher recyclable capacity (30 vs. truck-crane-1's 20) is exactly why job-bulk-recycling's demand of 22 fits on it but would have exceeded truck-crane-1's remaining room.

Multiple required_skills means AND, not OR

job-crane-hazmat's required_skills: ["crane", "hazmat"] means a resource must have every listed skill to be eligible — a resource with only crane or only hazmat would still be filtered out.

Next steps

FieldGenius VRP API documentation. Generated from the engineering source of truth (.tex docs and app/ source).