plansolve
v0.33.0
Published
Official JavaScript/TypeScript client for the [PlanSolve](https://getplansolve.com) optimization API. One fully typed client covers three solvers (field service routing, professional-services task assignment, and shift scheduling) with built-in polling an
Downloads
1,067
Readme
PlanSolve for JavaScript / TypeScript
Official JavaScript/TypeScript client for the PlanSolve optimization API. One fully typed client covers three solvers (field service routing, professional-services task assignment, and shift scheduling) with built-in polling and clean error messages. Runs anywhere JS does: Node.js, Deno, Bun, and edge runtimes.
Installation
npm install plansolveShips both ESM and CommonJS builds with bundled TypeScript declarations (for both import and require). Requires Node.js 18 or later (the SDK uses the global fetch).
PlanSolveClient is the default export and is also available as a named export, so import PlanSolveClient from "plansolve" and import { PlanSolveClient } from "plansolve" both work. With CommonJS, use const { PlanSolveClient } = require("plansolve").
Quick start
import PlanSolveClient from "plansolve";
const client = new PlanSolveClient("YOUR_API_KEY");
const request = {
vehicles: [
{
id: "tech1",
location: [40.7128, -74.006],
skills: ["repair"],
shifts: [
{ id: "morning", minStartTime: "2026-04-02T08:00:00", maxEndTime: "2026-04-02T17:00:00" },
],
},
],
visits: [
{
id: "visit1",
name: "AC Repair - Downtown Office",
location: [40.7589, -73.9851],
serviceDuration: "PT60M",
priority: "HIGH",
requiredSkills: ["repair"],
timeWindows: [
{ minStartTime: "2026-04-02T09:00:00", maxEndTime: "2026-04-02T17:00:00" },
],
},
],
};
// Submit and await the optimized plan in a single call
const result = await client.fieldService.startAndWaitForCompletion(request);
for (const vehicle of result.vehicles) {
console.log(`Vehicle ${vehicle.id}: ${vehicle.visits.length} visits`);
}Import
PlanSolveClientas a named export, not a default.
Solvers
One client, three solvers, all sharing 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 |
Each accessor exposes the same async methods:
| Method | HTTP | Description |
|--------|------|-------------|
| start(request) | POST /api/v1/{solver} | Submit a solve; resolves to a response with jobId. |
| getStatus(jobId) | GET /api/v1/{solver}/{jobId}/status | Point-in-time solver status. |
| getResult(jobId) | GET /api/v1/{solver}/{jobId} | The solved plan, with jobId stamped on it. |
| stop(jobId) | DELETE /api/v1/{solver}/{jobId} | Stop a running solve and return the best solution found so far (same shape as getResult). |
| analyze(jobId) | GET /api/v1/{solver}/{jobId}/analyze | Constraint analysis (score, per-constraint score and matches) as raw JSON. |
| waitForCompletion(jobId, pollIntervalMs?, maxAttempts?) | | Poll until the job is done, then return getResult. |
| startAndWaitForCompletion(request, pollIntervalMs?, maxAttempts?) | | start followed by waitForCompletion. |
{solver} is fieldservice, professionalservices or shift.
const { jobId } = await client.shift.start(request);
// ... later, if you do not want to wait any longer:
const best = await client.shift.stop(jobId);
const analysis = await client.shift.analyze(jobId);Score
Score is an exported interface with hard, medium and soft levels (number); a solution is feasible when hard >= 0. parseScore and formatScore are exported for converting to and from the canonical "Xhard/Ymedium/Zsoft" string.
Every Score this SDK hands back (i.e. anything produced by parseScore, including the score field on result and status responses) carries a hidden toJSON(), so JSON.stringify on a response serializes its score as that canonical string, e.g. "score":"0hard/0medium/-5soft" — never the raw {hard, medium, soft} object. That means a response you cache to disk or log as JSON can be read back with JSON.parse and fed straight back through the client (or parseScore directly) without throwing. score.hard/.medium/.soft still read normally, and Object.keys/spreading a score only ever shows those three fields — the toJSON is non-enumerable. A plain { hard, medium, soft } object literal you construct yourself still satisfies the Score type and reads back the same way; it just won't serialize to the canonical string, since it never went through parseScore.
Score fields on the result and status responses are optional (score?: Score): undefined while a job is still solving, and also for a job the solver has not scored yet.
const result = await client.fieldService.getResult(jobId);
if (result.score) {
console.log(`${formatScore(result.score)} (feasible: ${result.score.hard >= 0})`);
}Levels are number, not bigint, so values beyond 2^53 would lose precision — not reachable in practice. scoreString no longer exists on the Shift and Professional Services result responses; use score instead.
Polling
waitForCompletion and startAndWaitForCompletion wait pollIntervalMs before each status check and give up after maxAttempts checks. The defaults are the same for every solver: 5000 ms x 150 attempts (12.5 minutes, which covers the server's 10-minute solve cap). Values that are omitted or <= 0 fall back to these defaults.
A job is complete when its status reports solving: false and solverStatus: "NOT_SOLVING"; a score is not required.
Configuration
Pass your API key to the constructor: new PlanSolveClient("..."). It is sent as the X-API-KEY header on every request.
Error handling
Every failure throws a plain Error; the SDK never writes to the console.
- HTTP errors (any non-2xx response, from any method):
API error: status <code>: <message>, where the message comes from the API's error body (field validation messages first, thenerror,detailortitle). Example:API error: status 400: vehicles: At least one vehicle is required.A solve that fails on the server makes the status endpoint answer422, sowaitForCompletionrejects withAPI error: status 422: .... - Timeout (
waitForCompletion/startAndWaitForCompletion):Solver still running after <maxAttempts> polls; raise maxAttempts or lower options.spentLimit. - Empty job id (
waitForCompletion):JobId was not returned from waitForCompletion.
try {
const result = await client.fieldService.startAndWaitForCompletion(request);
} catch (e) {
console.error(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/javascript
- Professional Services: https://getplansolve.com/docs/professionalservices/sdk/javascript
- Shift: https://getplansolve.com/docs/shiftsolver/sdk/javascript
Package: npm
License
Apache-2.0. See LICENSE.
