Back to examples

Level overbooked resources with one click

Press Level resources and the engine removes every overbooking it can. It delays tasks, or, when splits are allowed, pauses a running task so the next one can start, and then reschedules everything that depended on them. The chart only draws the answer.

Leveling is the part of resource planning people distrust most, because it moves work they placed by hand. That is why the demo shows every change as data: a hatched area for a delay, a dashed line for a pause. Tick and untick Allow splits and compare the two plans.

One call

level solves the current plan, works through the overbooked days from the earliest one, and keeps the final plan as the accepted baseline. Like every other call it returns only what changed.

Level the plan
// One call. It solves, levels, and keeps the result as the accepted plan.
const result = project.level({ allowSplits });

base = merge(base, result.updates);   // dates, leveling_delay and pauses that changed
windows = result.overallocations;     // what could not be resolved, with a reason
baseId = result.baselineId;

Delay and split, not new dates

The engine does not hand back a hand-edited schedule. It sets two fields on the tasks it moves,leveling_delay and pauses, and the ordinary planner turns them into dates. The delay is the wait added before the task starts. Each pause is a break inside the task: where it starts, counted from the task start, and how long it lasts.

What leveling changes on a task
// Leveling never moves a bar by editing its dates. It writes two fields,
// and the planner turns them into dates. The project is created with
// new Project({ dates: "date" }), so the dates arrive as Date values.
result.updates = [
  { id: 2,  start_date: new Date(2026, 7, 12), leveling_delay: 2 },  // starts 2 days late
  { id: 10, end_date: new Date(2026, 8, 2),                          // works 11 days, pauses 11, then finishes
    pauses: [{ at: 11, length: 11, by: "leveling" }] },
  // …
];

The engine measures both in elapsed time, in the task's duration unit, where a day is 24 hours. So the demo draws them as calendar days with no conversion: a delay of three shows as three columns even across a weekend.

Draw the delay and pauses
// The demo reads the delay and pauses as calendar days and draws them as they are.
// leveling_delay: an area of N days before the start date
const left = Math.max(startDay - delay, 0) * DAY_W;

// pauses: [{ at, length }], both counted from the task start; work fills the rest up to the end
let day = startDay;
for (const { at, length } of pauses) {
  if (startDay + at > day) segments.push({ from: day, len: startDay + at - day, work: true });
  segments.push({ from: startDay + at, len: length, work: false });
  day = startDay + at + length;
}
segments.push({ from: day, len: endDay - day, work: true });

The load strip under the chart is not computed from the bars either. It draws the engine'sloadProfile: one bucket per day and person, with the load booked in it and the capacity. While you drag, the preview returns the same profile for the previewed plan, so the strip and the overbooked windows, in red, always come from the same answer.

Read the load the strip draws
// The load strip is the engine's own load: one bucket per day and person.
const profile = project.loadProfile({ from: "2026-08-10 00:00", to: "2026-10-05 00:00", basis: "day" });
// profile.resources: [{ resource: "ana", buckets: [{ start, load: 2, capacity: 1 }, …] }, …]

// During a drag the preview returns the profile of the previewed dates.
const preview = project.preview(changes, { baselineId, profile: { from, to, basis: "day" } });
// preview.profile has the same shape

Splits are a policy

With splits allowed, a task that is already running can be paused when a newcomer needs the resource, which avoids idle holes but fragments the task. Without them a task only ever moves whole. The engine decides who gives way by its selection order: by default a predecessor stays ahead of its successor, then the task with more slack moves, and a task that is already running is protected unless it can be paused.

Options are data
// Same call, different policy.
project.level({ allowSplits: true });    // pause a running task so a newcomer can start
project.level({ allowSplits: false });   // whole-task delays only

// other options: order, withinSlackOnly, basis, scope, step, clearFirst

Then keep working

Leveled plans stay editable. Drag a bar after leveling and the preview shows what your move does to the plan, including any new overbooking. Press Level again and the engine levels from scratch on top of your edit, because clearFirst is on by default. A reason is reported for anything it cannot fix, for example a fixed task or a constraint that does not allow the move.

The next demo turns to the other way to fix an overbooking: give the task to someone else, and let the engine tell you who.

The engine and its benchmarks are in the plea repository ↗