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 has either, then reschedule. For how leveling sets these fields, see Leveling resources.

Prerequisites#

  • A solved Project that has 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 have 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 object:

    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. The pauses change keeps the start and duration and recomputes the end.

  2. Apply the changes with apply and check the ApplyReport:

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

    const result = project.solve(null);
    

    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:

import { Project } from "plen-js";

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

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

const project = new Project();
project.solve(data);

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

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

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

const result = project.solve(null);
for (const update of result.updates) {
  console.log(`${update.id}: ${update.start_date} .. ${update.end_date}`);
}
for (const over of result.overallocations ?? []) {
  for (const window of over.windows ?? []) {
    console.log(
      `${over.resource}: ${window.from} .. ${window.to}, peak ${window.peak}`,
    );
  }
}

Note: The leveling result is a ScheduleResult with an added rounds field; read updates directly on it.

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. The run first sets leveling_delay: 0 and pauses: [] on every in-scope task that has either, then levels the clean plan:

const result = 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. Set clearFirst: false to keep the existing delays and pauses and level on top of them.

Things to know#

  • null also works: on leveling_delay it restores the default of 0, and on pauses it clears the list.
  • 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 changes to preview. The project stays untouched. See Previewing changes.
  • In a live editor, set baselineId on the change set to the baseline your model shows. apply then throws ProjectError with code "stale-baseline" if the project has moved on. See Error handling.
  • If apply rejects an operation, report.needsResync is true, and solve(null) throws ProjectError with code "needs-resync" until you send the full data with solve(data). See Recover from rejected changes.

Result#

The example 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