JsonProject.level#

Resolves resource overallocation in a JsonProject. It solves the project first, then writes persistent leveling_delay and pauses edits to tasks and accepts the leveled state as the baseline.

The project doesn't need to be solved beforehand. To see leveling without committing it, call level on a clone.

Signature#

level(options_json: string): string

Synchronous. Changes the project's state.

Request#

Argument Payload Required Description
options_json LevelingOptions yes Scope, order, basis and how leveling may move tasks. "{}" for defaults

Behavior#

  • The initial solve uses default solve options and works on the current state, including edits from apply. When the project is already solved with default options, the cached result is reused.
  • Forward scheduling only. A project with config.schedule_from_end: true is rejected.
  • On a cyclic project, level returns the initial solve's result with rounds: 0 and edits no task.
  • The leveled dates, delays and pauses replace the baseline, and baselineId in the result names the new baseline.
  • Tasks touched by successful apply calls since the last solve appear in updates as direct updates.

Response#

A success envelope with a LevelingResult.

LevelingResult#

The fields of a ScheduleResult, flattened into one object, plus rounds. analytics and profile aren't present in a level result.

Field Type Description
rounds number Number of leveling rounds that proposed a correction. 0 on a cyclic project
baselineId string The accepted baseline after leveling
updates Mutation[] Tasks that changed compared to the state the call started from: dates plus leveling_delay and pauses (snake_case). Empty on a cyclic project. See ScheduleResult
cycles Cycle[] Cycles found by the initial solve
conflicts Conflict[] Conflicts from the final scheduling pass
notes Note[] Notes from the final scheduling pass
converged boolean Whether the final scheduling pass reached a fixed point within max_iterations
overallocations Overallocation[] Overallocation left after leveling, each window with a reason. Present only when the project has assignments

Each updates entry can be passed back to apply as a task update.

Errors#

kind code When
parse null options_json isn't valid JSON, has a field of the wrong type (for example allowSplits: "no"), or has an unknown field (for example maxRounds)
validation null An option has a value it doesn't accept (for example step: "jump", path: null); an id in scope.resources or scope.tasks doesn't exist (path scope.resources / scope.tasks); the project has config.schedule_from_end: true (path config.schedule_from_end)
project needs-resync An earlier apply rejected an operation, and no solve with data has followed

Calling level on a freed handle throws at host level; see Wire format.

Example#

Level two tasks that share one resource, first on a copy, then for real, in Node:

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

const data = {
  config: { use_calendars: false },
  tasks: [
    { id: "a", start_date: "2026-08-10 00:00", duration: 2 },
    { id: "b", start_date: "2026-08-10 00:00", duration: 2 },
  ],
  resources: [{ id: "dev" }],
  assignments: [
    { task: "a", resource: "dev", value: 1 },
    { task: "b", resource: "dev", value: 1 },
  ],
};

const project = new JsonProject("{}");
try {
  const loaded = JSON.parse(project.solve(JSON.stringify(data), "{}"));
  if (!loaded.ok) throw new Error(loaded.error.message);

  // A proposal: level a copy, the project keeps its dates.
  const draft = project.clone();
  try {
    const proposal = JSON.parse(draft.level("{}"));
    if (proposal.ok) {
      for (const u of proposal.result.updates) {
        console.log(u.id, u.start_date, u.leveling_delay);
      }
    }
  } finally {
    draft.free();
  }

  // Commit: level the project itself.
  const envelope = JSON.parse(project.level("{}"));
  if (envelope.ok) {
    console.log(`${envelope.result.rounds} round(s)`);
  } else {
    console.error(envelope.error.kind, envelope.error.code, envelope.error.message);
  }
} finally {
  project.free();
}

Request (options_json):

{}

Response:

{
  "ok": true,
  "result": {
    "baselineId": "<opaque token>",
    "conflicts": [],
    "converged": true,
    "cycles": [],
    "notes": [],
    "overallocations": [],
    "rounds": 2,
    "updates": [
      {
        "id": "b",
        "kind": "task",
        "start_date": "2026-08-12 00:00",
        "end_date": "2026-08-14 00:00",
        "leveling_delay": 2
      }
    ]
  }
}

An unknown option fails the whole call:

{
  "ok": false,
  "error": {
    "kind": "parse",
    "code": null,
    "message": "unknown field `maxRounds`, expected one of `scope`, `basis`, `order`, `withinSlackOnly`, `clearFirst`, `allowSplits`, `step`",
    "path": null
  }
}