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 Project that carries leveling, either from a Level call or from loaded data with a non-zero LevelingDelay or non-empty Pauses.
  • The ids of the tasks that carry a delay or pauses. Take them from your own model, or from Schedule.Updates of the LevelingResult.

Steps#

  1. Build one TaskUpdate per leveled task. Set LevelingDelay to 0.0 and Pauses to an empty list, and put all the updates into one SchedulingChanges. Both properties are Patch<T> fields, and a plain value converts to Patch<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 LevelingDelay 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 Project.Apply and check the ApplyReport:

    var 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:

    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#

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