[Canonical page](<https://docs.dhtmlx.com/planning-engine/net/how-to/preview-leveling/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Preview leveling

Show the user what leveling would change before it touches the project, then keep the leveled plan or throw it away.

[`Project.Level`](<https://docs.dhtmlx.com/planning-engine/net/api/project-level.md>) commits its result: it writes `LevelingDelay` and `Pauses` into the project and accepts the leveled dates as the new baseline. To get a proposal instead, level a clone. [`Project.Clone`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/project.md#clone>) is cheap, because the copies share their data until one of them is edited.

## Prerequisites

- A solved [`Project`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/project.md>): [`Solve(data, …)`](<https://docs.dhtmlx.com/planning-engine/net/api/project-solve.md>) has run.
- The data has `Resources` and `Assignments`, so there's overallocation to level.
- The project is forward-scheduled. [`Level`](<https://docs.dhtmlx.com/planning-engine/net/api/project-level.md>) refuses [`Config.ScheduleFromEnd = true`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#schedulefromend>) with [`InputException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/input-exception.md>).

## Steps

1. Clone the solved project:

   ```csharp
   var draft = project.Clone();
   ```

2. Level the clone. The original keeps its dates, its baseline and its overallocation:

   ```csharp
   var proposal = draft.Level(new LevelingOptions());
   ```

3. Show the proposal. [`proposal.Schedule.Updates`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/leveling-result.md#schedule>) lists the tasks leveling moved, with their new dates, `LevelingDelay` and `Pauses`. The diff is relative to the state the call started from. That state matches the original, so you can apply the entries to the model you already hold.

   ```csharp
   foreach (var update in proposal.Schedule.Updates)
   {
       var pauses = update.Pauses is { } list
           ? string.Join(", ", list.Select(p => $"{p.At}+{p.Length}"))
           : "-";
       Console.WriteLine($"{update.Id}: start {update.StartDate}, delay {update.LevelingDelay}, pauses {pauses}");
   }
   ```

4. Show what leveling couldn't fix. [`proposal.Schedule.Overallocations`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#overallocations>) holds the windows left after leveling, and each window has a `Reason`, such as `fixed-task` or `priority-1000`:

   ```csharp
   foreach (var over in proposal.Schedule.Overallocations ?? [])
   {
       foreach (var window in over.Windows)
       {
           Console.WriteLine($"{over.Resource}: {window.From} .. {window.To}, {window.Reason}");
       }
   }
   ```

5. Apply the user's choice:
   - Accept: replace the original with the clone, `project = draft;`. The project now holds the leveled baseline.
   - Reject: drop `draft`. Nothing else needs undoing.

   Replace the original only if it hasn't been edited since you cloned it. Otherwise the proposal was computed from an older state, so level a new clone.

The full example levels two tasks that book the same developer full time on Monday and Tuesday:

```csharp
using Plen;

// Monday, August 10, 2026, on the default calendar.
var monday = Moment.FromYmdHm(2026, 8, 10, 8, 0) ?? throw new InvalidOperationException("invalid date");

var data = new ProjectData
{
    Tasks =
    [
        new TaskData("A") { StartDate = monday, Duration = 2.0 },
        new TaskData("B") { StartDate = monday, Duration = 2.0 },
    ],
    Resources = [new ResourceData { Id = "dev1" }],
    Assignments =
    [
        new AssignmentData { Task = "A", Resource = "dev1", Value = 1.0 },
        new AssignmentData { Task = "B", Resource = "dev1", Value = 1.0 },
    ],
};

var project = new Project();
project.Solve(data, new SolveOptions());

var draft = project.Clone();
var proposal = draft.Level(new LevelingOptions());

foreach (var update in proposal.Schedule.Updates)
{
    Console.WriteLine($"{update.Id}: {update.StartDate} .. {update.EndDate}, delay {update.LevelingDelay}");
}
var left = proposal.Schedule.Overallocations?.Count ?? 0;
Console.WriteLine($"overallocated resources left: {left}");

var userAccepts = true;
if (userAccepts)
{
    project = draft;
}
var plan = project.Solve(null, new SolveOptions());
Console.WriteLine($"baseline: {plan.BaselineId}");
```

## Result

The example prints the one task leveling moved, and nothing is left overallocated:

```text
B: 2026-08-12 08:00 .. 2026-08-14 08:00, delay 2
overallocated resources left: 0
```

`B` waits two days for `A`, so its `LevelingDelay` is `2.0`. Until you replace `project` with `draft`, `project` still reports the Monday-Tuesday window on `dev1`, and a [`Solve(null, …)`](<https://docs.dhtmlx.com/planning-engine/net/api/project-solve.md>) on it returns no updates. After the swap, [`BaselineId`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#baselineid>) is the token from the leveling result.

The clone is a full solved project, so you can also call [`draft.LoadProfile`](<https://docs.dhtmlx.com/planning-engine/net/api/project-load-profile.md>) on it to draw the leveled load. See [Resource load](<https://docs.dhtmlx.com/planning-engine/net/resources-and-assignment/resource-load.md>). To compare leveling across several alternative change sets with scores, use [`Evaluate`](<https://docs.dhtmlx.com/planning-engine/net/api/project-evaluate.md>) with [`EvaluateOptions.Level`](<https://docs.dhtmlx.com/planning-engine/net/api/core/evaluate/evaluate-options.md#level>) instead. See [Evaluating alternatives](<https://docs.dhtmlx.com/planning-engine/net/resources-and-assignment/evaluating-alternatives.md>). For options, reasons and splits, see [Leveling resources](<https://docs.dhtmlx.com/planning-engine/net/resources-and-assignment/leveling-resources.md>).
