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


# Leveling Resources

This guide shows how to remove resource overallocation from a plan. It covers what [`Project.Level`](<https://docs.dhtmlx.com/planning-engine/net/api/project-level.md>) changes, how it picks the task that gives way, how to limit it to part of the plan, and how to read, preview and undo its result.

## How leveling works

A resource is overallocated when the tasks booked on it need more than its capacity at the same time. [`Solve`](<https://docs.dhtmlx.com/planning-engine/net/api/project-solve.md>) only reports this, as [`Overallocations`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#overallocations>) windows on the result. [`Level`](<https://docs.dhtmlx.com/planning-engine/net/api/project-level.md>) fixes it by moving work later. It never changes assignments, resources or capacities. It changes two task fields:

- `LevelingDelay` delays the whole task.
- `Pauses` splits a task: it stops working for a while and picks up again later.

Leveling looks at the plan in **buckets**: day buckets when [`Config.DurationUnit`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#durationunit>) is [`TimeUnit.Day`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/time-unit.md>), hour buckets for `Hour` and `Minute`. The **load** of a resource in a bucket is the sum of `Value` over the assignments that book working time in that bucket. A task counts with its full `Value` as soon as it works any time in the bucket. When the load is higher than the resource's `Capacity` (by default [`Config.DefaultResourceCapacity`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#defaultresourcecapacity>), which is 1), the bucket is overallocated. A run of overallocated buckets in a row is a **window**.

`Level` works through the plan from left to right, in rounds:

1. Find the earliest overallocated bucket.
2. Collect the tasks that contribute to it, the **contenders**, and sort them by the selection order.
3. Move the weakest contender just past the bucket, with a delay or a pause, until the load fits.
4. Reschedule the project, so links and constraints push the successors along.
5. Repeat until no overallocated bucket with a movable contender is left.

Some tasks never move. Leveling leaves these alone and calls them the **fixed set**:

- tasks with `AutoScheduling = false`
- completed tasks (`Progress == 1`, when [`Config.UseProgress`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#useprogress>) is on)
- tasks with an `Mso` or `Mfo` constraint (when [`Config.ApplyConstraints`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#applyconstraints>) is on)
- tasks with `Priority == 1000`
- summaries (their dates follow their children)
- tasks outside [`LevelingScope.Tasks`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/leveling-options.md#levelingscope>), when you set it

Sometimes a constraint or another hard rule pulls a moved task back. Leveling then reverts the move and freezes the task for the rest of the run. Hard requirements always win. Whatever leveling can't resolve stays in `Overallocations`, and each remaining window gets a `Reason`.

A few rules hold for every run:

- `Level` commits. It solves the current state first, writes the delays and pauses into the project and accepts the leveled dates as the new baseline. To show leveling as a proposal, run it on a clone. See [Previewing and clearing](<https://docs.dhtmlx.com/planning-engine/net/resources-and-assignment/leveling-resources.md#previewing-and-clearing>).
- Leveling is forward-only. For a project with [`Config.ScheduleFromEnd = true`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#schedulefromend>), `Level` throws [`InputException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/input-exception.md>) at the path `config.schedule_from_end`.
- The output is deterministic. Every tie ends in input order, so the same data and options always give the same result.
- There's no round limit. A run ends when nothing is left to resolve. `Level` is synchronous and CPU-bound, and on large projects it can take a while. In a server or UI app, run it off the request or UI thread, for example with `Task.Run`. See [Call from async code](<https://docs.dhtmlx.com/planning-engine/net/how-to/call-from-async-code.md>).

## Leveling a project

Call [`Level`](<https://docs.dhtmlx.com/planning-engine/net/api/project-level.md>) with a default [`LevelingOptions`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/leveling-options.md>) after the project is solved. The examples on this page use the default calendar (Monday to Friday, 08:00-12:00 and 13:00-17:00) and start on Monday, August 10, 2026.

```csharp
using Plen;

static Moment Aug(int day) =>
    Moment.FromYmdHm(2026, 8, day, 8, 0) ?? throw new InvalidOperationException("invalid date");

static AssignmentData Booking(string task) =>
    new() { Task = task, Resource = "dev1", Value = 1.0 };

// Two 2-day tasks, both booked full time on dev1 from Monday.
var data = new ProjectData
{
    Tasks =
    [
        new TaskData("A") { StartDate = Aug(10), Duration = 2.0 },
        new TaskData("B") { StartDate = Aug(10), Duration = 2.0 },
    ],
    Resources = [new ResourceData { Id = "dev1" }],
    Assignments = [Booking("A"), Booking("B")],
};

var project = new Project();
var solved = project.Solve(data, new SolveOptions());
foreach (var over in solved.Overallocations ?? [])
    foreach (var window in over.Windows)
        // dev1: 2026-08-10 08:00 .. 2026-08-11 17:00, peak 2 of 1
        Console.WriteLine($"{over.Resource}: {window.From} .. {window.To}, peak {window.Peak} of {window.Capacity}");

var leveled = project.Level(new LevelingOptions());
foreach (var update in leveled.Schedule.Updates)
    // B: 2026-08-12 08:00 .. 2026-08-14 08:00, delay 2
    Console.WriteLine($"{update.Id}: {update.StartDate} .. {update.EndDate}, delay {update.LevelingDelay}");
```

[`Solve`](<https://docs.dhtmlx.com/planning-engine/net/api/project-solve.md>) reports one window on `dev1` over Monday and Tuesday, with a peak load of 2. `Level` keeps `A` and moves `B` to Wednesday. Both tasks are equal, so the input-order tie-break decides: the later task moves.

Leveling diffs its `Updates` against the state the call started from, so you can apply them to the model you already hold. Next to the new dates, each entry carries the task's new `LevelingDelay` and, when it changed, its complete `Pauses` list. A [`Mutation`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/mutation.md>) maps field-for-field onto a [`TaskUpdate`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/task-update.md>).

## Delays and pauses

`LevelingDelay` and `Pauses` are **elapsed** time, in the task's duration unit: with day units, 1 is 24 hours of calendar time, weekends and nights included. `Duration` and `Lag` stay working time.

- A delay shifts the task's start from its driven position (the start that links and constraints give it). Draw it as the stretch `[start - delay, start)` before the bar.
- A [`Pause`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/pause.md>) starts `At` after the task start and lasts `Length`. The leveler sets `By` to [`PauseSource.Leveling`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/pause.md#pausesource>) on every pause it writes. Draw pauses as gaps in the bar. They need no calendar.

Which one leveling picks depends on the contender. A task that starts inside the overallocated bucket has no work before it, so it gets a whole-task delay. A task that's already running in that bucket can be paused: it keeps the work it has done and resumes after the contention. Pausing a running task leaves no idle hole behind it. Delaying it would. Set `AllowSplits = false` to get whole-task delays only.

```csharp
using Plen;

static Moment Aug(int day) =>
    Moment.FromYmdHm(2026, 8, day, 8, 0) ?? throw new InvalidOperationException("invalid date");

static AssignmentData Booking(string task, string resource) =>
    new() { Task = task, Resource = resource, Value = 1.0 };

// A runs Monday and Tuesday. B joins on Tuesday and feeds C, so it has less slack.
var data = new ProjectData
{
    Tasks =
    [
        new TaskData("A") { StartDate = Aug(10), Duration = 2.0 },
        new TaskData("B") { StartDate = Aug(11), Duration = 2.0 },
        new TaskData("C") { StartDate = Aug(13), Duration = 5.0 },
    ],
    Links = [new LinkData { Id = "bc", Source = "B", Target = "C" }],
    Resources = [new ResourceData { Id = "dev1" }, new ResourceData { Id = "dev2" }],
    Assignments = [Booking("A", "dev1"), Booking("B", "dev1"), Booking("C", "dev2")],
};

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

// Splits allowed (the default): A is paused, B and C keep their dates.
var split = project.Clone().Level(new LevelingOptions());
foreach (var update in split.Schedule.Updates)
{
    // A: end 2026-08-14 08:00, pauses [1 + 2]
    var pauses = string.Join(", ", (update.Pauses ?? []).Select(p => $"{p.At} + {p.Length}"));
    Console.WriteLine($"{update.Id}: end {update.EndDate}, pauses [{pauses}]");
}

// Whole-task delays only: B moves past A, and C moves with it.
var whole = project.Clone().Level(new LevelingOptions { AllowSplits = false });
foreach (var update in whole.Schedule.Updates)
    // B: 2026-08-12 08:00 .. 2026-08-14 08:00
    // C: 2026-08-14 08:00 .. 2026-08-21 08:00
    Console.WriteLine($"{update.Id}: {update.StartDate} .. {update.EndDate}");
```

With splits, `A` keeps its start and works Monday, pauses for two days (`At` 1, `Length` 2), and finishes its second day on Thursday. `B` works in the pause, so the chain into `C` doesn't move. Without splits, `A` can't be paused and stays in place, so `B` is delayed whole and the link pushes `C` along. The update for `A` has a null `StartDate`, because its start didn't change.

Pauses only come from leveling: there are no user-defined pauses. A pause takes up no load, and a task without pauses gets a null `Pauses` in its update. The field rules are on the [`Pause` reference](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/pause.md>).

## Leveling options

[`LevelingOptions`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/leveling-options.md>) has a default for every property. Set the ones you need with an object initializer:

| Property | Default | What it changes |
|---|---|---|
| `Scope` | `null` (whole project) | Which resources, tasks and dates leveling acts on. |
| `Basis` | `null` (derived from [`Config.DurationUnit`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#durationunit>)) | Bucket size, [`Basis.Day`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/basis.md>) or `Basis.Hour`. |
| `Order` | [`LevelingOrder.Standard`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/leveling-options.md#levelingorder>) | Which contender moves first. See [Selection order](<https://docs.dhtmlx.com/planning-engine/net/resources-and-assignment/leveling-resources.md#selection-order>). |
| `WithinSlackOnly` | `false` | Never push a task past the project end of the plan leveling started from. |
| `ClearFirst` | `true` | Clear the existing delays and pauses on in-scope tasks before leveling. |
| `AllowSplits` | `true` | Allow `Pauses`. With `false`, every move is a whole-task delay. |
| `Step` | [`LevelingStep.Minimal`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/leveling-options.md#levelingstep>) | How far one move goes. |

The full list is on the [`LevelingOptions` reference](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/leveling-options.md>).

### Leveling part of a plan

[`LevelingScope`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/leveling-options.md#levelingscope>) limits a run in three ways, and you can combine them:

- `Tasks`: only these tasks may move. Everything else acts like a fixed task.
- `Resources`: only contention on these resources triggers a move.
- `From` / `To`: only buckets that start in `[From, To)` trigger a move. The range limits where leveling looks for contention, not where tasks land: a moved task may end up past `To`.

```csharp
using Plen;

static Moment Aug(int day, int hour) =>
    Moment.FromYmdHm(2026, 8, day, hour, 0) ?? throw new InvalidOperationException("invalid date");

static AssignmentData Booking(string task) =>
    new() { Task = task, Resource = "dev1", Value = 1.0 };

var data = new ProjectData
{
    Tasks =
    [
        new TaskData("A") { StartDate = Aug(10, 8), Duration = 2.0 },
        new TaskData("B") { StartDate = Aug(10, 8), Duration = 2.0 },
    ],
    Resources = [new ResourceData { Id = "dev1" }],
    Assignments = [Booking("A"), Booking("B")],
};
var project = new Project();
project.Solve(data, new SolveOptions());

// Only A may move, so A gives way even though B is later in input order.
var onlyA = new LevelingOptions { Scope = new LevelingScope { Tasks = ["A"] } };
var result = project.Clone().Level(onlyA);
foreach (var update in result.Schedule.Updates)
    // A: 2026-08-12 08:00 .. 2026-08-14 08:00
    Console.WriteLine($"{update.Id}: {update.StartDate} .. {update.EndDate}");

// Only contention from Wednesday on. The Monday-Tuesday window stays.
var fromWednesday = new LevelingOptions { Scope = new LevelingScope { From = Aug(12, 0) } };
result = project.Clone().Level(fromWednesday);
foreach (var over in result.Schedule.Overallocations ?? [])
    foreach (var window in over.Windows)
        // dev1: 2026-08-10 08:00 .. 2026-08-11 17:00, out-of-scope
        Console.WriteLine($"{over.Resource}: {window.From} .. {window.To}, {window.Reason}");
```

The second run leaves the Monday window alone and reports it with the reason `out-of-scope`. The result still says [`Converged == true`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#converged>), because the run did everything it was asked to do. The reason tells you why the window is still there.

An unknown id in `Scope.Tasks` or `Scope.Resources` fails the call with [`InputException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/input-exception.md>), at the path `scope.tasks` or `scope.resources`.

### Staying within slack

With [`WithinSlackOnly = true`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/leveling-options.md>), leveling never pushes a task further than its total slack. The slack is measured against the project end of the plan the run started from, so leveling can't make the project finish later. If a contender can't clear the bucket within its slack, leveling skips it and tries the next one. If no contender fits, the window stays with the reason `slack-exhausted`.

The slack leveling uses is internal. It ignores the existing delays and uses that fixed project end, so it can differ from [`Analytics.Slack`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/analytics.md#taskslack>) in a [`Solve`](<https://docs.dhtmlx.com/planning-engine/net/api/project-solve.md>) result.

### Fewer rounds with the skip step

By default a move clears one bucket. Then leveling reschedules and re-selects at the next bucket, so in a long window the task that gives way can change from bucket to bucket. [`Step = LevelingStep.Skip`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/leveling-options.md#levelingstep>) moves the task past the whole window in one move. That takes fewer rounds and reschedules less often. Use it for large projects where [`Level`](<https://docs.dhtmlx.com/planning-engine/net/api/project-level.md>) is slow.

```csharp
using Plen;

static Moment Aug(int day) =>
    Moment.FromYmdHm(2026, 8, day, 8, 0) ?? throw new InvalidOperationException("invalid date");

static AssignmentData Booking(string task) =>
    new() { Task = task, Resource = "dev1", Value = 1.0 };

var data = new ProjectData
{
    Tasks =
    [
        new TaskData("A") { StartDate = Aug(10), Duration = 2.0 },
        new TaskData("B") { StartDate = Aug(10), Duration = 2.0 },
    ],
    Resources = [new ResourceData { Id = "dev1" }],
    Assignments = [Booking("A"), Booking("B")],
};
var project = new Project();
project.Solve(data, new SolveOptions());

var minimal = project.Clone().Level(new LevelingOptions());
var skip = project.Clone().Level(new LevelingOptions { Step = LevelingStep.Skip });

// Same dates for B (Wednesday to Friday), in 2 rounds and in 1.
Console.WriteLine($"minimal: {minimal.Rounds} rounds, skip: {skip.Rounds} rounds");
Console.WriteLine(minimal.Schedule.Updates.SequenceEqual(skip.Schedule.Updates)); // True
```

Here both steps give the same plan. That's not guaranteed: buckets the skip step jumps over never get a new selection, so on real plans the two can level differently. If a jump hits a constraint or doesn't fit the slack, leveling falls back to the one-bucket move for that task.

## Selection order

When several contenders share an overallocated bucket, the order decides which one stays and which moves first. Every order ends in the same tie-break: the task later in input order moves.

| [`LevelingOrder`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/leveling-options.md#levelingorder>) | Who moves first |
|---|---|
| `Standard` (default) | Checked in order: a successor before its predecessor, more total slack, later start, lower `Priority`, no dated constraint. |
| `Priority` | Lower `Priority` first, then the `Standard` checks. |
| `Id` | The task later in input order. |

All three orders also apply the running rule. A task already working before the bucket stays, and the task that starts in the bucket moves, unless the running task can be paused instead. This keeps whole-task delays from leaving gaps in work that was already scheduled. Under `Priority`, a higher priority beats this rule: the lower-priority running task is paused, or delayed whole when splits are off.

`Priority` is a `ushort` on [`TaskData`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/task-data.md>), 500 by default. At 1000 the task never moves.

The dependency check exists because delaying a predecessor would push its successor along and resolve nothing. In this plan `B` comes first in the input, but an SS link from `A` makes `A` its predecessor, so `B` moves:

```csharp
using Plen;

static Moment Aug(int day) =>
    Moment.FromYmdHm(2026, 8, day, 8, 0) ?? throw new InvalidOperationException("invalid date");

static AssignmentData Booking(string task) =>
    new() { Task = task, Resource = "dev1", Value = 1.0 };

var data = new ProjectData
{
    Tasks =
    [
        new TaskData("B") { StartDate = Aug(10), Duration = 2.0 },
        new TaskData("A") { StartDate = Aug(10), Duration = 2.0 },
    ],
    Links = [new LinkData { Id = "ab", Source = "A", Target = "B", LinkType = LinkType.Ss }],
    Resources = [new ResourceData { Id = "dev1" }],
    Assignments = [Booking("A"), Booking("B")],
};
var project = new Project();
project.Solve(data, new SolveOptions());

var result = project.Level(new LevelingOptions());
foreach (var update in result.Schedule.Updates)
    // B: 2026-08-12 08:00 .. 2026-08-14 08:00
    Console.WriteLine($"{update.Id}: {update.StartDate} .. {update.EndDate}");
```

To protect a task from leveling, set its `Priority` to 1000 or its `AutoScheduling` to `false`. With `Priority = 1000` on `B` in the first example, `A` moves to Wednesday instead.

## Reading the result

[`Level`](<https://docs.dhtmlx.com/planning-engine/net/api/project-level.md>) returns a [`LevelingResult`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/leveling-result.md>):

- [`Schedule`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/leveling-result.md#schedule>): a [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md>), the same shape [`Solve`](<https://docs.dhtmlx.com/planning-engine/net/api/project-solve.md>) returns. Its `Updates` carry the new dates, delays and pauses. [`BaselineId`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#baselineid>) names the new baseline.
- [`Rounds`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/leveling-result.md#rounds>): the number of rounds that proposed a move. Rounds that only skipped an unresolvable bucket don't count.

Treat `Schedule` as read-only. The project keeps it as its baseline result and can hand the same object to later calls and clones. Call `DeepClone()` before you modify it.

[`Schedule.Converged`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#converged>) is the scheduler's convergence on the final plan, not a leveling flag. Leveling itself always runs until nothing is left to resolve.

`Schedule.Overallocations` lists the windows that remain (see [`ResourceOverallocation`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/resource-overallocation.md>)). Every window from `Level` has a `Reason`. When a window has several immovable contenders, the first reason in this table wins:

| `Reason` | Why the window remains |
|---|---|
| `pinned-constraint` | A contender was frozen because its move didn't stick, or it has an `Mso` / `Mfo` constraint. |
| `fixed-task` | A contender has `AutoScheduling = false`, or is a summary. |
| `completed` | A contender has `Progress == 1` under `Config.UseProgress`. |
| `priority-1000` | A contender has `Priority == 1000`. |
| `slack-exhausted` | Every candidate was capped by `WithinSlackOnly`. |
| `out-of-scope` | The resource, the dates or every contender is outside `Scope`. |

A reason is data, not an error. It tells the user what to change to resolve the window: unpin a task, loosen a constraint, widen the scope.

```csharp
using Plen;

static Moment Aug(int day) =>
    Moment.FromYmdHm(2026, 8, day, 8, 0) ?? throw new InvalidOperationException("invalid date");

static AssignmentData Booking(string task) =>
    new() { Task = task, Resource = "dev1", Value = 1.0 };

// A is fixed. B is the only task that could move, but it must start by Monday.
var data = new ProjectData
{
    Tasks =
    [
        new TaskData("A") { StartDate = Aug(10), Duration = 2.0, AutoScheduling = false },
        new TaskData("B")
        {
            StartDate = Aug(10),
            Duration = 2.0,
            ConstraintType = ConstraintType.Snlt,
            ConstraintDate = Aug(10),
        },
    ],
    Resources = [new ResourceData { Id = "dev1" }],
    Assignments = [Booking("A"), Booking("B")],
};
data.Config.ApplyConstraints = true;

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

// The move was reverted, so nothing changed.
Console.WriteLine(result.Schedule.Updates.Count); // 0
foreach (var over in result.Schedule.Overallocations ?? [])
    foreach (var window in over.Windows)
        // dev1: tasks [A, B], reason pinned-constraint
        Console.WriteLine($"{over.Resource}: tasks [{string.Join(", ", window.Tasks)}], reason {window.Reason}");
```

Leveling proposed a delay for `B`, the replan pulled `B` back to Monday, and leveling reverted the move. Nothing in the project changed. The window stays and reports `pinned-constraint`. With both tasks fixed and no constraint, the same window would report `fixed-task`.

The other window properties (`From`, `To`, `Capacity`, `Peak`, `Overload`, `Tasks`) are on the [`ResourceOverallocation` reference](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/resource-overallocation.md>). For the load of each bucket, see [Resource load](<https://docs.dhtmlx.com/planning-engine/net/resources-and-assignment/resource-load.md>).

`Level` throws [`InputException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/input-exception.md>) for an unknown scope id or a backward-scheduled project, and [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/project-exception.md>) with [`Code == ProjectError.NeedsResync`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/project-exception.md#projecterror>) after a rejected [`Apply`](<https://docs.dhtmlx.com/planning-engine/net/api/project-apply.md>). Both leave the project as it was. See [Error handling](<https://docs.dhtmlx.com/planning-engine/net/error-handling.md>).

## Previewing and clearing

Because [`Level`](<https://docs.dhtmlx.com/planning-engine/net/api/project-level.md>) commits, run it on a clone to show leveling as a proposal. [`Project.Clone()`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/project.md#clone>) is cheap: the data is shared until one copy is edited.

```csharp
using Plen;

static Project LevelingProposal(Project project)
{
    var draft = project.Clone();
    var proposal = draft.Level(new LevelingOptions());
    foreach (var update in proposal.Schedule.Updates)
        Console.WriteLine($"{update.Id}: delay {update.LevelingDelay}, pauses {update.Pauses?.Count ?? 0}");
    // Accept: use the returned draft in place of project. Reject: drop it.
    return draft;
}
```

The step-by-step version is in [Preview leveling](<https://docs.dhtmlx.com/planning-engine/net/how-to/preview-leveling.md>). To score leveling as part of an alternative change set, 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>). See [Evaluating alternatives](<https://docs.dhtmlx.com/planning-engine/net/resources-and-assignment/evaluating-alternatives.md>).

There are two ways to remove leveling:

- To undo it, set `LevelingDelay` to 0 and `Pauses` to an empty list on the task, then call [`Solve(null, …)`](<https://docs.dhtmlx.com/planning-engine/net/api/project-solve.md>). The original contention comes back.
- To level again from scratch, call `Level` again. [`ClearFirst`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/leveling-options.md>) is on by default, so the run first clears the delays and pauses of the in-scope tasks and levels from the clean plan. With `ClearFirst = false` the existing delays and pauses stay, and leveling adds to them. Use that to keep a deliberate gap the user set by hand.

```csharp
using Plen;

static Moment Aug(int day) =>
    Moment.FromYmdHm(2026, 8, day, 8, 0) ?? throw new InvalidOperationException("invalid date");

static AssignmentData Booking(string task) =>
    new() { Task = task, Resource = "dev1", Value = 1.0 };

var data = new ProjectData
{
    Tasks =
    [
        new TaskData("A") { StartDate = Aug(10), Duration = 2.0 },
        new TaskData("B") { StartDate = Aug(10), Duration = 2.0 },
    ],
    Resources = [new ResourceData { Id = "dev1" }],
    Assignments = [Booking("A"), Booking("B")],
};
var project = new Project();
project.Solve(data, new SolveOptions());
project.Level(new LevelingOptions()); // B moves to Wednesday

// Undo leveling on B.
var changes = new SchedulingChanges();
changes.Tasks.Update.Add(new TaskUpdate("B") { LevelingDelay = 0.0, Pauses = new List<Pause>() });
project.Apply(changes);

var replanned = project.Solve(null, new SolveOptions());
foreach (var update in replanned.Updates)
    // B: 2026-08-10 08:00 .. 2026-08-12 08:00
    Console.WriteLine($"{update.Id}: {update.StartDate} .. {update.EndDate}");

// dev1 is overallocated on Monday and Tuesday again.
Console.WriteLine((replanned.Overallocations ?? []).Any(o => o.Windows.Count > 0)); // True
```

The `LevelingDelay` update moves the stored start of `B` back by the same amount, so `B` returns to Monday and the window over Monday and Tuesday comes back. To clear every task, put one such update per task with a delay or pauses into the same change set. The full recipe is in [Clear leveling](<https://docs.dhtmlx.com/planning-engine/net/how-to/clear-leveling.md>).
