PlanSolve 0.34.0

dotnet add package PlanSolve --version 0.34.0
                    
NuGet\Install-Package PlanSolve -Version 0.34.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PlanSolve" Version="0.34.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PlanSolve" Version="0.34.0" />
                    
Directory.Packages.props
<PackageReference Include="PlanSolve" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PlanSolve --version 0.34.0
                    
#r "nuget: PlanSolve, 0.34.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PlanSolve@0.34.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PlanSolve&version=0.34.0
                    
Install as a Cake Addin
#tool nuget:?package=PlanSolve&version=0.34.0
                    
Install as a Cake Tool

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:

Package: NuGet

License

Apache-2.0

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

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