PlanSolve 0.34.0
dotnet add package PlanSolve --version 0.34.0
NuGet\Install-Package PlanSolve -Version 0.34.0
<PackageReference Include="PlanSolve" Version="0.34.0" />
<PackageVersion Include="PlanSolve" Version="0.34.0" />
<PackageReference Include="PlanSolve" />
paket add PlanSolve --version 0.34.0
#r "nuget: PlanSolve, 0.34.0"
#:package PlanSolve@0.34.0
#addin nuget:?package=PlanSolve&version=0.34.0
#tool nuget:?package=PlanSolve&version=0.34.0
PlanSolve for .NET
Official .NET client for the PlanSolve optimization API. One typed client covers three solvers (field service routing, professional-services task assignment, and shift scheduling) with async/await, built-in polling, and clean error messages.
Installation
dotnet add package PlanSolve
Targets .NET 10.
Quick start
using PlanSolve;
using PlanSolve.FieldService;
using var client = new PlanSolveClient("YOUR_API_KEY");
var request = new FieldServiceStartRequest
{
Vehicles =
{
new Vehicle
{
Id = "tech1",
Location = [40.7128, -74.0060],
Skills = ["repair"],
Shifts = [new Shift("morning", DateTime.Parse("2026-04-02T08:00:00"), DateTime.Parse("2026-04-02T17:00:00"))],
},
},
Visits =
{
new Visit
{
Id = "visit1",
Name = "AC Repair - Downtown Office",
Location = [40.7589, -73.9851],
ServiceDuration = Duration.FromMinutes(60),
Priority = "HIGH",
RequiredSkills = ["repair"],
},
},
};
// Submit and await the optimized plan in a single call
var result = await client.FieldService.StartAndWaitForCompletionAsync(request);
foreach (var vehicle in result.Vehicles)
Console.WriteLine($"Vehicle {vehicle.Id}: {vehicle.Visits.Count} visits");
Every vehicle shift needs an Id. Priority on a visit or task is optional: leave it null and the solver plans the item as MEDIUM.
Solvers
One client, three solvers. All share the same submit, poll, result workflow:
| Solver | Accessor | Use for |
|---|---|---|
| Field Service | client.FieldService |
Vehicle routing with travel time, time windows, and skills |
| Professional Services | client.ProfessionalServices |
Task assignment by skill, availability, priority, and deadlines |
| Shift | client.Shift |
Shift scheduling across contracts, availability, and fairness |
Every accessor exposes the same methods:
| Method | HTTP | Returns |
|---|---|---|
StartAsync(request) |
POST /api/v1/{solver} |
Start response with the JobId |
GetStatusAsync(jobId) |
GET /api/v1/{solver}/{jobId}/status |
SolverStatusResponse |
GetResultAsync(jobId) |
GET /api/v1/{solver}/{jobId} |
The solver's result type |
StopAsync(jobId) |
DELETE /api/v1/{solver}/{jobId} |
The best solution found so far (same type as GetResultAsync); throws JobCancelledException for a job that was still queued |
GetAnalysisAsync(jobId) |
GET /api/v1/{solver}/{jobId}/analyze |
AnalysisResponse (constraint breakdown) |
WaitForCompletionAsync(jobId, ...) |
polls status, then fetches the result | The solver's result type |
StartAndWaitForCompletionAsync(request, ...) |
start + wait | The solver's result type |
GetStatusAsync, GetResultAsync, StopAsync and GetAnalysisAsync also have an overload that takes a CancellationToken.
// Stop a long-running solve early and keep the best plan found so far
try
{
var best = await client.FieldService.StopAsync(jobId);
}
catch (JobCancelledException e)
{
// The job was still waiting in the queue: it is cancelled, no plan exists and nothing is billed.
Console.WriteLine($"{e.JobId}: {e.Message}");
}
// Explain the score: which constraints were broken, and by what
var analysis = await client.FieldService.GetAnalysisAsync(jobId);
Score
Score is a struct with Hard, Medium and Soft levels (long). A solution is feasible when Hard >= 0. Parse it from the canonical "Xhard/Ymedium/Zsoft" string with Score.Parse/TryParse, or rely on the implicit conversions to and from string.
Score fields on the result and status responses are Score?: null while a job is still solving, and also for a job the solver has not scored yet.
var result = await client.FieldService.GetResultAsync(jobId);
if (result.Score is { } score)
Console.WriteLine($"{score} (feasible: {score.Hard >= 0})");
AnalysisResponse.Score, ConstraintAnalysis.Weight, ConstraintAnalysis.Score and ConstraintMatch.Score are also Score?: a missing or unparseable score on one constraint or match yields null for that field rather than failing the whole analysis, since analysis is a diagnostic surface where partial data still has value.
scoreString no longer exists on the Shift and Professional Services result responses — use the typed Score instead.
Shift results
ShiftResultResponse.AssignedShifts and UnassignedShifts are lists of shift names (a shift with Value > 1 is listed once per position, as "{name}#1", "{name}#2"...). The shifts themselves, with the employee the solver chose in AssignedEmployee, are in Shifts:
var result = await client.Shift.GetResultAsync(jobId);
foreach (var shift in result.Shifts ?? [])
Console.WriteLine($"{shift.Name}: {shift.From} -> {shift.To} ({shift.AssignedEmployee ?? "unassigned"})");
Jobs
client.Jobs lists and reads job records. JobDto.Status is a JobStatus (Pending, Succeeded, Failed, Lost, Cancelled); JobDto.SolverType is a number you can compare with SolverTypes.FieldService (0), SolverTypes.Shift (1) and SolverTypes.ProfessionalServices (2).
Polling
WaitForCompletionAsync and StartAndWaitForCompletionAsync poll the status endpoint every pollIntervalMs milliseconds, up to maxAttempts times. The defaults are 5000 ms x 150 attempts (12.5 minutes), which covers the server's 10 minute solve cap with headroom. Passing a value of 0 or less uses the default.
A job is complete when the status reports Solving == false and SolverStatus == NOT_SOLVING. The result is then fetched with GetResultAsync. Status polls during the wait, and result fetches, are retried on transient failures only (a network error, or a 408, 429 or 5xx answer), up to 3 times with 2, 4 and 8 second backoff, before the error is thrown. Any other error, such as 401, 404 or 422, is thrown on the first response. StopAsync is not retried. The library never writes to the console.
Configuration
Pass your API key to the constructor: new PlanSolveClient("...") (an overload accepts a custom HttpClient). It is sent as the X-API-KEY header on every request. PlanSolveClient is IDisposable, so wrap it in using.
Error handling
| Exception | When |
|---|---|
HttpRequestException |
The API returned a non-2xx status. .StatusCode is populated and the message is readable, e.g. Request failed with status code 400 (BadRequest): vehicles: At least one vehicle is required. A failed solve answers the status endpoint with 422, which surfaces here while waiting. |
JobCancelledException |
StopAsync reached a job that was still waiting in the queue. The job is cancelled (the stop worked; do not retry it) and no plan exists. .JobId names the job. |
ArgumentException |
jobId is null or empty (message: jobId cannot be null or empty. Pass the JobId returned by StartAsync., ParamName is jobId). |
TimeoutException |
The solver is still running after maxAttempts polls (message: Solver still running after N polls; raise maxAttempts or lower options.spentLimit.). |
OperationCanceledException |
The CancellationToken you passed was cancelled (may be the TaskCanceledException subtype). Cancellations are never retried. |
System.Text.Json.JsonException |
A response body could not be deserialized. The message includes the JSON path and a truncated copy of the body. |
try
{
var result = await client.FieldService.StartAndWaitForCompletionAsync(request);
}
catch (HttpRequestException e)
{
Console.WriteLine($"API error {(int?)e.StatusCode}: {e.Message}");
}
catch (TimeoutException e)
{
Console.WriteLine(e.Message);
}
Documentation
Full guides, per-solver data models, and parameter reference live on the docs site:
- Field Service: https://getplansolve.com/docs/fieldservice/sdk/dotnet
- Professional Services: https://getplansolve.com/docs/professionalservices/sdk/dotnet
- Shift: https://getplansolve.com/docs/shiftsolver/sdk/dotnet
Package: NuGet
License
Apache-2.0
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Polly (>= 8.6.5)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.34.0 | 0 | 10/13/2026 |
| 0.33.0 | 324 | 10/3/2026 |
| 0.32.0 | 86 | 10/3/2026 |
| 0.31.0 | 87 | 10/3/2026 |
| 0.30.0 | 91 | 10/3/2026 |
| 0.29.0 | 99 | 9/22/2026 |
| 0.28.0 | 99 | 9/22/2026 |
| 0.27.0 | 130 | 7/23/2026 |
| 0.26.0 | 120 | 7/22/2026 |
| 0.25.8 | 127 | 7/22/2026 |
| 0.25.7 | 118 | 7/21/2026 |
| 0.25.6 | 117 | 7/21/2026 |
| 0.25.5 | 123 | 7/21/2026 |
| 0.25.4 | 122 | 7/21/2026 |
| 0.25.3 | 115 | 7/21/2026 |
| 0.25.2 | 121 | 7/21/2026 |
| 0.25.1 | 110 | 7/21/2026 |
| 0.25.0 | 118 | 7/21/2026 |