Visit profit
Field service companies often have more work available than their technicians can complete in a day. When capacity is limited, it matters which visits get scheduled.
By assigning a profit value to a visit, Timefold can prioritize visits that generate the most revenue. When a technician’s shift fills up, lower-profit visits are left unscheduled in favor of higher-profit visits.
This is particularly useful when visits are optional, visits whose time window extends beyond the planning window, so they can be deferred to a future day without a hard constraint violation.
1. Fixed profit
Learn how to configure an API Key to run the examples in this guide:
In the examples, replace |
A visit’s profit property contains a fixedProfit field representing the revenue gained when that visit is completed.
{
"visits": [
{
"id": "Boiler replacement",
"location": [33.77301, -84.43838],
"serviceDuration": "PT2H",
"profit": {
"fixedProfit": 500
}
}
]
}
The fixedProfit is a flat amount of profit attributed to a visit, regardless of how long the visit takes or which technician completes it.
Timefold rewards scheduling a visit with fixedProfit set.
The higher the value, the stronger the incentive to include that visit in the plan.
When a shift has room for only some of the available optional visits, Timefold selects the combination that maximizes the total profit across all scheduled visits.
2. Example: prioritizing profitable work
A plumbing company has one technician available on the morning of February 1st, 2027. The shift allows two hours of service (from 09:00 to 11:00), but there are three optional visits on the backlog, each taking one hour.
The boiler replacement (500) and the pipe repair (300) is the most profitable visit and is scheduled. The low-value faucet check (50) is left unscheduled.
-
Input
-
Output
Try this example in Timefold Platform by saving this JSON into a file called sample.json and making the following API call:
|
curl -X POST -H "Content-type: application/json" -H 'X-API-KEY: <API_KEY>' https://app.timefold.ai/api/models/field-service-routing/v1/route-plans [email protected]
{
"config": {
"run": {
"name": "Visit profit example"
}
},
"modelInput": {
"planningWindow": {
"startDate": "2027-02-01T00:00:00Z",
"endDate": "2027-02-02T00:00:00Z"
},
"vehicles": [
{
"id": "Bob",
"shifts": [
{
"id": "Bob-2027-02-01",
"startLocation": [33.77301, -84.43838],
"endLocation": [33.77301, -84.43838],
"minStartTime": "2027-02-01T09:00:00Z",
"maxLastVisitDepartureTime": "2027-02-01T11:00:00Z"
}
]
}
],
"visits": [
{
"id": "Boiler replacement",
"location": [33.77301, -84.43838],
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-03T17:00:00Z"
}
],
"serviceDuration": "PT1H",
"profit": {
"fixedProfit": 500
}
},
{
"id": "Pipe repair",
"location": [33.77301, -84.43838],
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-03T17:00:00Z"
}
],
"serviceDuration": "PT1H",
"profit": {
"fixedProfit": 300
}
},
{
"id": "Faucet check",
"location": [33.77301, -84.43838],
"timeWindows": [
{
"minStartTime": "2027-02-01T09:00:00Z",
"maxEndTime": "2027-02-03T17:00:00Z"
}
],
"serviceDuration": "PT1H",
"profit": {
"fixedProfit": 50
}
}
]
}
}
To request the solution, locate the ID from the response to the post operation and append it to the following API call:
|
curl -X GET -H 'X-API-KEY: <API_KEY>' https://app.timefold.ai/api/models/field-service-routing/v1/route-plans/<ID>
{
"metadata": {
"id": "ID",
"name": "Visit profit example",
"submitDateTime": "2025-02-17T10:15:28.589040604Z",
"startDateTime": "2025-02-17T10:15:34.610314505Z",
"activeDateTime": "2025-02-17T10:15:34.87683868Z",
"completeDateTime": "2025-02-17T10:20:35.024965784Z",
"shutdownDateTime": "2025-02-17T10:20:35.299293025Z",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/0medium/3800000soft",
"tags": [
"system.profile:default"
],
"validationResult": {
"summary": "OK"
}
},
"modelOutput": {
"vehicles": [
{
"id": "Bob",
"shifts": [
{
"id": "Bob-2027-02-01",
"startTime": "2027-02-01T09:00:00Z",
"itinerary": [
{
"id": "Pipe repair",
"arrivalTime": "2027-02-01T09:00:00Z",
"startServiceTime": "2027-02-01T09:00:00Z",
"departureTime": "2027-02-01T10:00:00Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT0S",
"travelDistanceMetersFromPreviousStandstill": 0,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"kind": "VISIT"
},
{
"id": "Boiler replacement",
"arrivalTime": "2027-02-01T10:00:00Z",
"startServiceTime": "2027-02-01T10:00:00Z",
"departureTime": "2027-02-01T11:00:00Z",
"effectiveServiceDuration": "PT1H",
"travelTimeFromPreviousStandstill": "PT0S",
"travelDistanceMetersFromPreviousStandstill": 0,
"minStartTravelTime": "2027-02-01T00:00:00Z",
"kind": "VISIT"
}
],
"metrics": {
"totalServiceDuration": "PT2H",
"totalBreakDuration": "PT0S",
"totalWaitingTime": "PT0S",
"totalTravelTime": "PT0S",
"travelTimeFromStartLocationToFirstVisit": "PT0S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT0S",
"totalTravelDistanceMeters": 0,
"travelDistanceFromStartLocationToFirstVisitMeters": 0,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 0,
"endLocationArrivalTime": "2027-02-01T11:00:00Z",
"overtime": "PT0S",
"availableOvertime": "PT0S",
"totalProfit": 800
}
}
],
"metrics": {
"activatedShifts": 1,
"assignedVisits": 2,
"totalShiftDuration": "PT2H",
"totalServiceDuration": "PT2H",
"totalTravelTime": "PT0S",
"totalTravelDistanceMeters": 0,
"totalBreakTime": "PT0S",
"totalWaitingTime": "PT0S",
"totalOvertime": "PT0S",
"availableOvertime": "PT0S",
"totalProfit": 800
}
}
],
"unassignedVisits": [
"Faucet check"
]
},
"inputMetrics": {
"vehicles": 1,
"vehicleShifts": 1,
"visits": 3,
"mandatoryVisits": 0,
"optionalVisits": 3,
"pinnedVisits": 0,
"visitsWithSla": 0,
"visitGroups": 0,
"visitDependencies": 0,
"excludedVisits": 0,
"movableVisits": 3
},
"kpis": {
"averageTravelTimePerVisit": "PT0S",
"totalTravelTime": "PT0S",
"travelTimeFromStartLocationToFirstVisit": "PT0S",
"travelTimeBetweenVisits": "PT0S",
"travelTimeFromLastVisitToEndLocation": "PT0S",
"averageTravelDistanceMetersPerVisit": 0,
"totalTravelDistanceMeters": 0,
"travelDistanceFromStartLocationToFirstVisitMeters": 0,
"travelDistanceBetweenVisitsMeters": 0,
"travelDistanceFromLastVisitToEndLocationMeters": 0,
"totalUnassignedVisits": 1,
"totalAssignedVisits": 2,
"assignedMandatoryVisits": 0,
"unassignedMandatoryVisits": 0,
"assignedOptionalVisits": 2,
"unassignedOptionalVisits": 1,
"totalActivatedVehicles": 1,
"workingTimeFairnessPercentage": 100.0,
"totalOvertime": "PT0S",
"availableOvertime": "PT0S",
"totalProfit": 800
}
}
modelOutput contains the two most profitable visits in Bob’s shift itinerary.
The unassignedVisits list contains Faucet check, the least profitable visit that could not fit within the shift’s capacity.
3. Maximize profit constraint
The Maximize profit soft constraint rewards each scheduled visit that has a positive fixedProfit.
The reward is proportional to the profit value, so a visit worth 500 contributes ten times more to the score than a visit worth 50.
This means that when choosing which optional visits to schedule, Timefold naturally selects the combination that yields the highest total revenue, even when the individually most profitable visit is a worse fit than a pair of shorter, moderately profitable visits.
fixedProfit must be zero or greater.
Negative values are rejected with a validation error.
|
Every soft constraint has a weight that can be configured to change the relative importance of the constraint compared to other constraints. Learn about constraint weights. |
Next
-
See the full API spec or try the online API.
-
Learn more about field service routing from our YouTube playlist.
-
Read Priority visits and optional visits to learn how to make visits optional so that less profitable work can be deferred.
-
Read Technician costs to learn how to minimize the cost of a visit alongside maximizing profit.