JsonProject.suggestAssignments#

Proposes resources for requirement slots of a JsonProject. It fills empty slots, repairs broken ones and rebalances valid ones, and scores each step like evaluate. The project isn't modified: the proposal comes back as a change set to commit with apply. Needs a solved project with no edits applied since the last solve or level.

Signature#

suggestAssignments(slots_json: string, options_json: string): string

Synchronous and read-only. Bad input comes back as an error envelope. The call doesn't throw.

Request#

Argument Payload Required Description
slots_json Slot[] yes The slots to fill, repair or rebalance. slots builds them from skills and requirements, and its output can be passed as is. "[]" returns an empty proposal
options_json SuggestOptions yes "{}" for defaults

SuggestOptions#

The options object for suggestAssignments. Every field can be omitted, and null reads as the default. Unknown fields, such as mode, are rejected with a parse error.

Field Type Required Default Description
baselineId string no absent (not checked) Must name the current baseline, or the call fails with stale-baseline
targets string[] no ["empty"] Which slots may change, judged by the slot's current resource: "empty" (no assignment; filled), "broken" (resource not in allowed; repaired), "valid" (resource in allowed; rebalanced). Each value at most once. [] changes no assignment and only reports invalid
fixed Id[] no [] Assignment ids that are never changed. A slot bound to one of them is never visited
newResourcePenalty number no 0 Cost added for each resource used beyond the reference state, >= 0. 0 balances load only; a large value favors the fewest people
level LevelingOptions | false no false With leveling options, every candidate is leveled and scored on the leveled stage, and proposed.leveled is filled. false scores the scheduled stage. true is a validation error
passes number no 1 Rebalance passes over valid slots, >= 1. Used only when targets contains "valid"
clearFirst boolean no false Scores against a copy with in-scope leveling_delay / pauses cleared. The clears are added to changes.tasks.update. Separate from level.clearFirst

Behavior#

  • Greedy: slots are visited one at a time, and later slots build on earlier picks.
  • It never removes an assignment. It adds one per filled slot, with the slot id as the assignment id, and changes the resource of a repaired or rebalanced one.
  • A slot that can't be filled or repaired isn't an error: it's listed in unfilled or invalid.
  • Commit the proposal with apply(JSON.stringify(result.changes)), then solve without data, or level with the same leveling options when level was set.

How picks are made and scored: Suggesting assignments.

Response#

The success envelope carries a SuggestResult.

SuggestResult#

Field Type Description
baselineId string The accepted baseline the call scored against
changes SchedulingChanges The net proposal, with changes.baselineId set. assignments.add entries are {id, task, resource, value}, assignments.update entries are {id, resource}. With clearFirst, tasks.update holds {id, leveling_delay: 0, pauses: []} per cleared task. Empty buckets and unset fields are omitted, so a proposal with no change is {baselineId} alone. Pass it to apply unchanged
steps SuggestStep[] The decisions that changed the proposal, in order
unfilled Id[] Targeted empty slots left empty, in visit order
invalid Id[] Slots whose final resource isn't in allowed, fixed slots included, in visit order
baseline { cost, resources } Cost and number of used resources of the reference state, on the scoring stage
proposed SuggestProposal The proposed state measured against the reference state

SuggestStep#

Field Type Description
slot Id The slot that changed
task Id The slot's task
pass number 0 for a fill or a repair, 1 and up for a rebalance pass
fromResource Id | null The previous resource. null for a fill
toResource Id The chosen resource
cost { before, after } Cost of the state before and after the step, penalty included. before is the previous step's after, or baseline.cost for the first step

SuggestProposal#

Field Type Description
cost number Cost of the proposed state, penalty included
resources number Number of used resources in the proposed state
scheduled Metrics Scheduled-stage metrics against the reference state
leveled LeveledMetrics Leveled-stage metrics. Present only when options.level is set

Errors#

Kind Code When
project no-baseline The project has never been solved, or apply ran since the last solve
project stale-baseline options.baselineId doesn't name the current baseline
validation null Invalid options: newResourcePenalty negative or not finite (newResourcePenalty), passes below 1 (passes), a duplicate in targets (targets[i]), an unknown, inactive or duplicate fixed entry (fixed[i]), invalid level options
validation null The accepted state has a cycle (path: null)
validation null An invalid slot: duplicate id (slots[i].id), unknown, unscheduled or summary task (slots[i].task), unknown allowed resource (slots[i].allowed[j]), value not finite or <= 0 (slots[i].value), an assignment that is unknown, inactive, on another task or bound by two slots (slots[i].assignment), a slot id equal to another existing assignment's id (slots[i].id)
validation null A value the decoder rejects, with path: null: an unknown targets value, level: true
parse null Malformed JSON, a wrong JSON type, or an unknown field in a slot or the options

The JSON is decoded before the project is checked. After that, no-baseline and stale-baseline come first, then the options, the cycle check and the slots, in the order above. The accepted slot values are on the Slot page.

Example#

Fill an open position on task C, then commit the proposal.

const { JsonProject } = require("./pkg-node/plen_wasm.js");

const data = {
  config: { use_calendars: false },
  tasks: [
    { id: "A", start_date: "2026-08-10", duration: 2 },
    { id: "C", start_date: "2026-08-10", duration: 2 },
  ],
  resources: [{ id: "dev1" }, { id: "dev2" }],
  assignments: [{ id: "aA", task: "A", resource: "dev1", value: 1 }],
};

const project = new JsonProject("{}");
try {
  const solved = JSON.parse(project.solve(JSON.stringify(data), "{}")).result;

  // Task C needs one person: dev1 or dev2.
  const slots = [{ id: "sC", task: "C", allowed: ["dev1", "dev2"] }];
  const options = { baselineId: solved.baselineId };
  const response = JSON.parse(
    project.suggestAssignments(JSON.stringify(slots), JSON.stringify(options)),
  );

  if (response.ok) {
    for (const step of response.result.steps) {
      console.log(`${step.slot} -> ${step.toResource}`); // sC -> dev2
    }
    const report = JSON.parse(project.apply(JSON.stringify(response.result.changes)));
    if (report.ok && report.result.rejected.length === 0) {
      project.solve(undefined, "{}");
    }
  } else {
    const { kind, code, message, path } = response.error;
    console.error(kind, code, message, path);
  }
} finally {
  project.free();
}

slots_json:

[{ "id": "sC", "task": "C", "allowed": ["dev1", "dev2"] }]

options_json (the token comes from the solve result):

{ "baselineId": "1-b0605af8cba3d7c9" }

Response:

{
  "ok": true,
  "result": {
    "baseline": { "cost": 0, "resources": 1 },
    "baselineId": "1-b0605af8cba3d7c9",
    "changes": {
      "assignments": {
        "add": [{ "id": "sC", "resource": "dev2", "task": "C", "value": 1 }]
      },
      "baselineId": "1-b0605af8cba3d7c9"
    },
    "invalid": [],
    "proposed": {
      "cost": 0,
      "resources": 2,
      "scheduled": {
        "conflicts": { "delta": 0, "value": 0 },
        "cost": 0,
        "cycles": { "delta": 0, "value": 0 },
        "finish": {
          "project": { "date": "2026-08-12 00:00", "delta": 0 },
          "tasks": []
        },
        "overload": {
          "focus": [],
          "resources": [],
          "total": { "delta": 0, "value": 0 }
        }
      }
    },
    "steps": [
      {
        "cost": { "after": 0, "before": 0 },
        "fromResource": null,
        "pass": 0,
        "slot": "sC",
        "task": "C",
        "toResource": "dev2"
      }
    ],
    "unfilled": []
  }
}

dev2 wins with cost 0. With dev1 the step would cost 2: the overload from working on A and C on the same days.