Clear leveling#
Remove the delays and splits that Project.Level wrote into a project, so every task goes back to its unleveled dates.
Leveling stores its result in two task fields: LevelingDelay and Pauses. To clear leveling, set LevelingDelay to 0 and Pauses to an empty list on every task that has either, then reschedule. For how leveling sets these fields, see Leveling resources.
Prerequisites#
- A solved
Projectthat carries leveling, either from aLevelcall or from loaded data with a non-zeroLevelingDelayor non-emptyPauses. - The ids of the tasks that carry a delay or pauses. Take them from your own model, or from
Schedule.Updatesof theLevelingResult.
Steps#
-
Build one
TaskUpdateper leveled task. SetLevelingDelayto0.0andPausesto an empty list, and put all the updates into oneSchedulingChanges. Both properties arePatch<T>fields, and a plain value converts toPatch<T>.Of(value):static SchedulingChanges ClearLeveling(IEnumerable<Id> ids) { var changes = new SchedulingChanges(); foreach (var id in ids) { changes.Tasks.Update.Add(new TaskUpdate(id) { LevelingDelay = 0.0, Pauses = new List<Pause>(), }); } return changes; }Leave the dates out of the update. With no date in the same update, the
LevelingDelaychange moves the stored start back by the delay, and thePauseschange keeps the start and duration and recomputes the end. -
Apply the changes with
Project.Applyand check theApplyReport:var report = project.Apply(ClearLeveling(ids)); if (report.NeedsResync) { // Resync with project.Solve(data, ...) before the next edit. } -
Reschedule with
Solve(null, …)and writeresult.Updatesback to your model:var result = project.Solve(null, new SolveOptions());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:
using Plen;
static Moment Aug(int day) =>
Moment.FromYmdHm(2026, 8, day, 8, 0) ?? throw new InvalidOperationException("invalid date");
static TaskData Task(string id) => new(id) { StartDate = Aug(10), Duration = 2.0 };
static AssignmentData Booking(string task) =>
new() { Task = task, Resource = "dev1", Value = 1.0 };
static SchedulingChanges ClearLeveling(IEnumerable<Id> ids)
{
var changes = new SchedulingChanges();
foreach (var id in ids)
{
changes.Tasks.Update.Add(new TaskUpdate(id)
{
LevelingDelay = 0.0,
Pauses = new List<Pause>(),
});
}
return changes;
}
var data = new ProjectData
{
Tasks = [Task("A"), Task("B"), Task("C")],
Resources = [new ResourceData { Id = "dev1" }],
Assignments = [Booking("A"), Booking("B"), Booking("C")],
};
var project = new Project();
project.Solve(data, new SolveOptions());
// B is delayed to Wednesday, C to Friday.
var leveled = project.Level(new LevelingOptions());
// The tasks that leveling delayed or split.
var ids = leveled.Schedule.Updates
.Where(u => (u.LevelingDelay is double d && d != 0.0) || u.Pauses is { Count: > 0 })
.Select(u => u.Id)
.ToList();
var report = project.Apply(ClearLeveling(ids));
if (report.NeedsResync)
{
Console.Error.WriteLine("clearing was rejected, resync with Solve(data, ...)");
return;
}
var result = project.Solve(null, new SolveOptions());
foreach (var update in result.Updates)
{
Console.WriteLine($"{update.Id}: {update.StartDate} .. {update.EndDate}");
}
foreach (var over in result.Overallocations ?? [])
{
foreach (var window in over.Windows)
{
Console.WriteLine($"{over.Resource}: {window.From} .. {window.To}, peak {window.Peak}");
}
}
Re-level from scratch instead#
To replace old leveling with a fresh run, skip the steps above and call Level again. LevelingOptions.ClearFirst is true by default, so the run first sets LevelingDelay to 0 and Pauses to an empty list on every in-scope task that has either, then levels the clean plan:
var result = project.Level(new LevelingOptions());
The call diffs result.Schedule.Updates against the state it started from. A task whose stale delay was cleared and not replaced still gets an update entry. Write all the updates back, and your model stays 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#
Patch<T>.Nullalso works: onLevelingDelayit restores the default of 0, and onPausesit clears the list.- Only leveling produces pauses. Every
Pausecounts as leveling-owned, whether or not itsByis set, so clearing removes all of them. - To see what clearing would do before you commit it, pass the same changes to
Project.Preview. The project stays untouched. See Previewing changes. - In a live editor, set
changes.BaselineIdto the baseline your model shows.Applythen throws aProjectExceptionwithCode == ProjectError.StaleBaselineif the project has moved on. See Error handling. - If
Applyrejects an operation,report.NeedsResyncistrue, andSolve(null, …)throwsProjectError.NeedsResyncuntil you send the full data withSolve(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