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.
// 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.
// 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.
// 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.
// 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 shapeSplits 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.
// 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, clearFirstThen 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.