Project#suggestAssignments#

Proposes resources for requirement slots of a Project. It fills empty slots, repairs broken ones and rebalances valid ones, scoring each step like evaluate. The project isn't modified: the proposal comes back as a change set. Needs a solved project.

Signature#

suggestAssignments(slots: Slot[], options?: SuggestOptions): SuggestResult

Synchronous. Runs on the calling thread.

Parameters#

Name Type Description
slots Slot[] The slots to fill, repair or rebalance. To build them from skills and requirements, use slots; its output can be passed as is.
options SuggestOptions Optional. Targets, fixed assignments, penalty, passes, scoring stage and baseline check. Default {}

SuggestOptions#

Options for one suggestAssignments call. Every field is optional. Unknown keys are rejected.

Field Type Default Description
baselineId string no check Expected baseline token, taken from the latest result's baselineId. A mismatch throws ProjectError "stale-baseline".
targets ("empty" | "broken" | "valid")[] ["empty"] Slot categories to act on, in any combination: "empty" fills slots with no assignment, "broken" repairs slots held by a resource outside allowed, "valid" rebalances slots held by a resource in allowed. [] changes no assignment and only reports invalid. Each value may appear once.
fixed Id[] [] Assignment ids that are never changed. A slot bound to one of them is never visited; its resource still counts as in use, and the slot is still reported in invalid when broken. An id bound to no slot is accepted.
newResourcePenalty number 0 Cost added per counted resource in use that the reference state doesn't use. 0 scores pure balance; a large value favors the fewest people. Included in proposed.cost and in each step's cost. Must be finite and >= 0.
level LevelingOptions | false false Leveling options: every candidate is leveled with them and scored on the leveled stage, and proposed.leveled is filled. false scores the scheduled stage.
passes number 1 Maximum number of rebalance passes over "valid" slots. No effect unless targets contains "valid". Must be >= 1.
clearFirst boolean false true builds and measures the proposal on a copy of the accepted state with leveling_delay and pauses cleared on the tasks in scope (level.scope.tasks, otherwise every task). The clears are returned in changes.tasks.update. Independent of the clearFirst of level.

For how targets, passes and the penalty drive the search, see Suggesting assignments.

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 and changes the resource of a repaired or rebalanced one. A new assignment takes the slot's id.
  • Commit the proposal with apply(result.changes), then solve(null), or level with the same leveling options when level was set.
  • slots and options are converted like every other input; see Plain objects and dates.

Returns#

A SuggestResult with the proposed changes and a step log, a fresh object that shares nothing with the input or the project. With dates: "date" on the project, the finish dates in proposed.scheduled and proposed.leveled are local Date objects.

SuggestResult#

The proposal, scored against the accepted state. With clearFirst, baseline and proposed describe the cleared state, while baselineId still names the accepted state.

Field Type Description
baselineId string Baseline token of the accepted state the proposal was scored against.
changes SchedulingChanges Net difference between the accepted state and the proposal, ready for apply. See below.
steps SuggestStep[] Decisions that changed the working state, in the order they were made. Empty when nothing moved.
unfilled Id[] Ids of targeted empty slots left empty, in visit order. Empty slots that weren't targeted aren't listed.
invalid Id[] Ids of slots whose final resource isn't in their allowed list, in visit order. Includes fixed slots and is filled whatever the targets.
baseline { cost: number; resources: number } Cost and number of resources in use of the reference state on the scoring stage (leveled when level is set).
proposed { cost: number; resources: number; scheduled: Metrics; leveled?: LeveledMetrics } Final state: cost including the penalty, resources in use, and Metrics against the reference state. leveled is present only when level is set.

changes holds:

  • baselineId equal to the result's baselineId.
  • One assignment add { id, task, resource, value } per filled slot, with the slot's id. A slot filled and then moved in a later pass is one add with its final resource.
  • One assignment update { id, resource } per repaired or rebalanced assignment, however many passes moved it. An assignment that ends on its original resource produces no entry.
  • With clearFirst, one task update { id, leveling_delay: 0, pauses: [] } per cleared task, ahead of the assignment edits, whenever something was cleared.

Empty buckets are left out, so with no steps and no clears changes is { baselineId } alone.

SuggestStep#

One decision that changed the working state.

Field Type Description
slot Id The slot's id.
task Id The slot's task.
pass number 0 for a fill or a repair, 1 and up for the rebalance pass that made the move.
fromResource Id | null The previous resource; null for a fill.
toResource Id The new resource.
cost { before: number; after: number } State cost including the penalty, on the scoring stage, before and after this step. The first step's before equals baseline.cost.

Errors#

Error When
ProjectError "no-baseline" The project is unsolved, including after apply.
ProjectError "stale-baseline" options.baselineId doesn't name the current baseline.
InputError "validation" Invalid options (newResourcePenalty, passes, targets[i], fixed[i], level); a cyclic accepted state (path: null); an invalid slot (paths under slots[i]: .id, .task, .allowed[j], .value, .assignment).
InputError "parse" An unknown key in options or in a slot.
TypeError The input has a circular reference or a BigInt.
Error("Project has been disposed") The project was disposed.

Options are checked before slots. The accepted slot values are on the Slot page.

Example#

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

import { Project } from "plen-js";

const project = new Project();
project.solve({
  config: { use_calendars: false },
  tasks: [
    { id: "A", start_date: "2026-08-10 00:00", duration: 2 },
    { id: "C", start_date: "2026-08-10 00:00", duration: 2 },
  ],
  resources: [{ id: "dev1" }, { id: "dev2" }],
  assignments: [{ id: "aA", task: "A", resource: "dev1", value: 1 }],
});

// Task C needs one person: dev1 or dev2.
const slots = [{ id: "sC", task: "C", allowed: ["dev1", "dev2"] }];
const result = project.suggestAssignments(slots);
for (const step of result.steps) {
  console.log(`${step.slot} -> ${step.toResource}`); // sC -> dev2
}

const report = project.apply(result.changes);
if (report.rejected.length === 0) {
  project.solve(null);
}