Clear leveling#

Remove the delays and splits that level wrote into a project, so every task goes back to its unleveled dates.

Leveling stores its result in two task fields: leveling_delay and pauses. To clear leveling, set leveling_delay to 0 and pauses to [] on every task that carries either, then reschedule. For how leveling sets these fields, see Leveling resources.

Prerequisites#

  • A solved JsonProject that carries leveling, either from a level call or from loaded data with a non-zero leveling_delay or non-empty pauses.
  • The ids of the tasks that carry a delay or pauses. Take them from your own model, or from the updates of the level result.

Steps#

  1. Build one TaskUpdate per leveled task, with leveling_delay: 0 and pauses: []. Put all the updates into one SchedulingChanges:

    function clearLeveling(ids) {
      return {
        tasks: {
          update: ids.map((id) => ({ id, leveling_delay: 0, pauses: [] })),
        },
      };
    }
    

    Leave the dates out of the update. With no date in the same update, the leveling_delay change moves the stored start back by the delay, and the pauses change keeps the start and duration and recomputes the end.

  2. Apply the changes with apply and check the report in the envelope's result:

    const report = unwrap(project.apply(JSON.stringify(clearLeveling(ids))));
    if (report.needsResync) {
      // Resync with project.solve(dataJson, ...) before the next edit.
    }
    
  3. Reschedule with solve without data, and write result.updates back to your model:

    const result = unwrap(project.solve(undefined, "{}"));
    

    The cleared dates become the accepted baseline. The resource contention that leveling resolved shows up again in result.overallocations.

The full example below levels three 2-day tasks that share dev1, then clears the leveling. It uses the default calendar (Monday to Friday, 08:00-12:00 and 13:00-17:00) and starts on Monday, August 10, 2026:

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

function unwrap(text) {
  const response = JSON.parse(text);
  if (!response.ok) {
    const { kind, code, message, path } = response.error;
    throw new Error(`${kind} ${code ?? ""}: ${message} (at ${path})`);
  }
  return response.result;
}

function clearLeveling(ids) {
  return {
    tasks: {
      update: ids.map((id) => ({ id, leveling_delay: 0, pauses: [] })),
    },
  };
}

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

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

  // B is delayed to Wednesday, C to Friday.
  const leveled = unwrap(project.level("{}"));

  // The tasks that leveling delayed or split.
  const ids = leveled.updates
    .filter((u) => (u.leveling_delay ?? 0) !== 0 || (u.pauses ?? []).length > 0)
    .map((u) => u.id);

  const report = unwrap(project.apply(JSON.stringify(clearLeveling(ids))));
  if (report.needsResync) {
    throw new Error("clearing was rejected, resync with solve(dataJson, ...)");
  }

  const result = unwrap(project.solve(undefined, "{}"));
  for (const u of result.updates) {
    console.log(`${u.id}: ${u.start_date} .. ${u.end_date}`);
  }
  for (const over of result.overallocations ?? []) {
    for (const w of over.windows ?? []) {
      console.log(`${over.resource}: ${w.from} .. ${w.to}, peak ${w.peak}`);
    }
  }
} finally {
  project.free();
}

The level call returns these updates, the input for the filter:

[
  { "id": "B", "kind": "task", "start_date": "2026-08-12 08:00", "end_date": "2026-08-14 08:00", "leveling_delay": 2 },
  { "id": "C", "kind": "task", "start_date": "2026-08-14 08:00", "end_date": "2026-08-18 08:00", "leveling_delay": 4 }
]

Re-level from scratch instead#

To replace old leveling with a fresh run, skip the steps above and call level again. clearFirst is true by default, so the run first sets leveling_delay: 0 and pauses: [] on every in-scope task that carries either, then levels the clean plan:

const result = unwrap(project.level("{}"));

The call diffs its updates against the state it started from. A task whose stale delay was cleared and not replaced still gets an update entry, so writing updates back to your model keeps it in sync.

With a scope, the run clears only in-scope tasks. Send {"clearFirst": false} to keep the existing delays and pauses and level on top of them.

Things to know#

  • null also works: leveling_delay: null restores the default of 0, and pauses: null clears the list. Omitting a field leaves it unchanged.
  • Only leveling produces pauses. Every pause counts as leveling-owned, whether or not its by is set, so clearing removes all of them.
  • To see what clearing would do before you commit it, pass the same change set to preview. The project stays untouched. See Previewing changes.
  • In a live editor, set baselineId in the change set to the baseline your model shows. apply then returns a project error with code stale-baseline if the project has moved on. See Error handling.
  • If apply rejects an operation, report.needsResync is true, and solve without data returns needs-resync until you send the full data with solve(dataJson, …). See Recover from rejected changes.

Result#

Run the example with node. It prints B and C back on Monday, and the overallocation window on dev1 returns:

B: 2026-08-10 08:00 .. 2026-08-12 08:00
C: 2026-08-10 08:00 .. 2026-08-12 08:00
dev1: 2026-08-10 08:00 .. 2026-08-11 17:00, peak 3