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


# Resources and assignments

This guide shows how to tell Planning Engine who does the work: resources with a capacity, assignments that book them on tasks, and slices that book only part of a task. It also shows how to read the overallocation windows that every scheduled plan with assignments reports.

## How resources work

Three records describe staffing. They live in [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/project-data.md>) next to the tasks:

- A **resource** ([`ResourceData`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/resource-data.md>)) is a person, a team or a machine. It has an `Id`, an optional `Capacity` and an optional `CalendarId`.
- An **assignment** ([`AssignmentData`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/assignment-data.md>)) books one resource on one task at a utilization rate, [`Value`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/assignment-data.md#value>). `1.0` means 100 %, so `0.5` is half of someone's time.
- A **slice** is an optional [`Offset`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/assignment-data.md#offset-duration>) / `Duration` pair on an assignment. With it, the assignment books only part of the task's work instead of all of it.

`Capacity` uses the same units as `Value`. A resource with `Capacity = 2` can take two full-time assignments at once, like a team of two. A resource with `Capacity = null` gets [`Config.DefaultResourceCapacity`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#defaultresourcecapacity>), which is `1.0` by default.

To check load, the engine cuts the timeline into **buckets**, hours or days. The basis comes from [`Config.DurationUnit`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#durationunit>): days for a [`TimeUnit.Day`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/time-unit.md>) project, hours for `Hour` and `Minute`. The **load** of a resource in a bucket is the sum of the `Value`s of its assignments whose task has working time in that bucket. A bucket **overallocates** when its load is strictly greater than the capacity. Two half-time assignments on a capacity of `1.0` are fine. A third one isn't.

`Value` is a rate, not a number of hours. Any working time of a task inside a bucket counts the assignment's full `Value` there.

Assignments don't move tasks. Task dates come from links, constraints and calendars, and assignments only decide where load lands. Calendars are the one exception: in the default [`CalendarMode.Resource`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/calendar-mode.md>), each assignment works on its resource's calendar, so a resource calendar can shift a task. See [Resource calendars](<https://docs.dhtmlx.com/planning-engine/net/scheduling/working-time-calendars.md#resource-calendars>).

Every schedule of a project with assignments fills [`ScheduleResult.Overallocations`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#overallocations>): one [`ResourceOverallocation`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/resource-overallocation.md>) per contended resource, each with a list of [`OverallocationWindow`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/resource-overallocation.md#overallocationwindow>)s. Overallocation is data, not an exception. Removing it is the job of [leveling](<https://docs.dhtmlx.com/planning-engine/net/resources-and-assignment/leveling-resources.md>).

[`Project.Solve(data, …)`](<https://docs.dhtmlx.com/planning-engine/net/api/project-solve.md>) and [`Engine.Schedule`](<https://docs.dhtmlx.com/planning-engine/net/api/engine-schedule.md>) validate resources and assignments when you pass the data. These cases throw an [`InputException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/input-exception.md>) whose `Path` names the field:

- a duplicate resource id (`resources[i].id`)
- a `Capacity` that isn't finite or is below `0` (`resources[i].capacity`)
- a `CalendarId` that names no calendar, in `CalendarMode.Resource` (`resources[i].calendar_id`)
- a duplicate assignment id (`assignments[i].id`)
- an assignment `Value` that isn't finite or is `0` or less (`assignments[i].value`)
- an assignment that names an unknown task or resource (`assignments[i].task`, `assignments[i].resource`)
- an invalid slice: `Offset` below `0` or not finite, `Duration` of `0` or less or not finite (`assignments[i].offset`, `assignments[i].duration`)

With [`Validation.Repair`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/validation.md>) the engine clears an invalid slice and adds a [`Note.Repaired`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/note.md#repaired>) instead, so the assignment books the whole task.

## Resources and capacity

Give each resource a `Capacity` that matches what it can take. Here one developer and a QA team of two both work on two tasks that run at the same time:

```csharp
using Plen;

AssignmentData Assign(string task, string resource, double value) =>
    new() { Id = $"{task}:{resource}", Task = task, Resource = resource, Value = value };

var monday = Moment.FromYmdHm(2026, 8, 10, 8, 0) ?? throw new InvalidOperationException("invalid date");

var data = new ProjectData
{
    // Two 2-day tasks, both starting Monday 2026-08-10.
    Tasks =
    [
        new TaskData("api") { StartDate = monday, Duration = 2 },
        new TaskData("ui") { StartDate = monday, Duration = 2 },
    ],
    Resources =
    [
        // One developer: Capacity falls back to Config.DefaultResourceCapacity (1.0).
        new ResourceData { Id = "dev" },
        // A QA team of two people.
        new ResourceData { Id = "qa", Capacity = 2 },
    ],
    Assignments =
    [
        Assign("api", "dev", 1),
        Assign("ui", "dev", 1),
        Assign("api", "qa", 1),
        Assign("ui", "qa", 1),
    ],
};

var result = Engine.Schedule(data, new SolveOptions());
foreach (var resource in result.Overallocations ?? [])
{
    foreach (var window in resource.Windows)
        Console.WriteLine($"{resource.Resource}: {window.From} -> {window.To}, peak {window.Peak} of {window.Capacity}");
}
```

Output:

```text
dev: 2026-08-10 08:00 -> 2026-08-11 17:00, peak 2 of 1
```

Both resources carry a load of `2` on Monday and Tuesday. Only `dev` is overallocated, because its capacity is the default `1.0`. `qa` sits exactly at its capacity, which isn't overallocation.

To change the default for every resource without a `Capacity`, set [`Config.DefaultResourceCapacity`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#defaultresourcecapacity>). With `8`, for example, a `Value` of `4` is half of a resource. A resource with its own `Capacity` ignores the default.

In a live [`Project`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/project.md>), change capacity with a [`ResourceUpdate`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/resource-update.md>) in [`SchedulingChanges.Resources.Update`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/scheduling-changes.md#tasks-links-resources-assignments>). [`Capacity = Patch<double>.Null`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/patch.md#null>) goes back to the default. Removing a resource also removes all its assignments, and [`ApplyReport`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/apply-report.md>) lists them in [`Effects.RemovedAssignments`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/apply-report.md#effects>).

## Assignments

An assignment links a task and a resource by id. One task can have several assignments, and one resource can work on several tasks. The example below books Ana full-time on two parallel tasks and the lead half-time on each, then moves the UI work to Ben:

```csharp
using Plen;

AssignmentData Assign(string task, string resource, double value) =>
    new() { Id = $"{task}:{resource}", Task = task, Resource = resource, Value = value };

void Report(string label, ScheduleResult result)
{
    var busy = (result.Overallocations ?? []).Select(r => r.Resource.ToString());
    Console.WriteLine($"{label}: overallocated [{string.Join(", ", busy)}]");
}

var monday = Moment.FromYmdHm(2026, 8, 10, 8, 0) ?? throw new InvalidOperationException("invalid date");

var data = new ProjectData
{
    Tasks =
    [
        new TaskData("api") { StartDate = monday, Duration = 2 },
        new TaskData("ui") { StartDate = monday, Duration = 2 },
    ],
    Resources = [new ResourceData { Id = "ana" }, new ResourceData { Id = "ben" }, new ResourceData { Id = "lead" }],
    Assignments =
    [
        Assign("api", "ana", 1),
        Assign("ui", "ana", 1),
        // The lead spends half of their time on each task.
        Assign("api", "lead", 0.5),
        Assign("ui", "lead", 0.5),
    ],
};

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

// Move the UI work from Ana to Ben.
var changes = new SchedulingChanges();
changes.Assignments.Update.Add(new AssignmentUpdate("ui:ana") { Resource = Patch<Id>.Of("ben") });
var applied = project.Apply(changes);
if (applied.Rejected.Count > 0)
    throw new InvalidOperationException($"{applied.Rejected[0].Path}: {applied.Rejected[0].Message}");

Report("after reassign", project.Solve(null, new SolveOptions()));
```

Output:

```text
initial: overallocated [ana]
after reassign: overallocated []
```

The lead's two half-time assignments add up to `1.0`, so only Ana shows up at first. The [`AssignmentUpdate`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/assignment-update.md>) changes the resource of `ui:ana`. [`Patch<Id>.Of("ben")`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/patch.md#of>) sets the field; a plain `"ben"` doesn't convert to `Patch<Id>`. Reassigning work doesn't change the task dates here. It changes them only when the task ends up on a different calendar.

Other assignment edits go through the same [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/scheduling-changes.md>):

- `Assignments.Add` takes an [`AssignmentData`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/assignment-data.md>). With `Id = null` the engine generates an id and reports it in [`ApplyReport.Effects.GeneratedAssignments`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/apply-report.md#effects>).
- `Assignments.Update` can change `Task`, `Resource`, `Value`, `Offset` and `Duration`.
- `Assignments.Remove` takes assignment ids. Removing a task removes its assignments too.

An assignment on an unscheduled task isn't deleted, but it leaves the active plan. It books no load, and the result carries a [`Note.DroppedAssignment`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/note.md#droppedassignment>). When the task is scheduled again, the assignment comes back with its slice. A milestone has no span, so its assignments book nothing.

## Assignment slices

By default an assignment books the whole task. Set `Offset` and `Duration` to book only part of it, for example for a reviewer who joins on the last day. Both are **worked time** in the task's [`DurationUnit`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#durationunit>), measured on the assignment's calendar:

- `Offset`: where the slice starts, counted from the task start. `null` means `0`.
- `Duration`: how much work the slice books. `null` means up to the task end.

The example runs the same project twice. The lead reviews the 4-day `build` task, first for all of it, then only on its last working day:

```csharp
using Plen;

ProjectData MakeProject(double? reviewOffset, double? reviewDuration)
{
    var monday = Moment.FromYmdHm(2026, 8, 10, 8, 0) ?? throw new InvalidOperationException("invalid date");
    return new ProjectData
    {
        Tasks =
        [
            // `build`: 4 working days, Monday to Thursday.
            new TaskData("build") { StartDate = monday, Duration = 4 },
            // `plan`: Monday only.
            new TaskData("plan") { StartDate = monday, Duration = 1 },
        ],
        Resources = [new ResourceData { Id = "dev" }, new ResourceData { Id = "lead" }],
        Assignments =
        [
            new AssignmentData { Id = "build:dev", Task = "build", Resource = "dev", Value = 1 },
            // The lead reviews `build`: the whole task, or only a slice of it.
            new AssignmentData
            {
                Id = "build:lead", Task = "build", Resource = "lead", Value = 1,
                Offset = reviewOffset, Duration = reviewDuration,
            },
            new AssignmentData { Id = "plan:lead", Task = "plan", Resource = "lead", Value = 1 },
        ],
    };
}

var options = new SolveOptions { IncludeUnchanged = true };
// Whole task, then the last working day only (offset 3 days, 1 day long).
foreach (var (offset, duration) in new (double?, double?)[] { (null, null), (3, 1) })
{
    var result = Engine.Schedule(MakeProject(offset, duration), options);
    Console.WriteLine($"offset {offset?.ToString() ?? "null"}, duration {duration?.ToString() ?? "null"}");
    foreach (var update in result.Updates)
    {
        if (update.StartDate is { } start && update.EndDate is { } end)
            Console.WriteLine($"  {update.Id}: {start} -> {end}");
    }
    foreach (var resource in result.Overallocations ?? [])
    {
        foreach (var window in resource.Windows)
            Console.WriteLine($"  {resource.Resource} overallocated: {window.From} -> {window.To}");
    }
}
```

Output:

```text
offset null, duration null
  build: 2026-08-10 08:00 -> 2026-08-14 08:00
  plan: 2026-08-10 08:00 -> 2026-08-11 08:00
  lead overallocated: 2026-08-10 08:00 -> 2026-08-10 17:00
offset 3, duration 1
  build: 2026-08-10 08:00 -> 2026-08-14 08:00
  plan: 2026-08-10 08:00 -> 2026-08-11 08:00
```

Without a slice, the lead is booked on `build` and `plan` on Monday. With `Offset = 3`, `Duration = 1` the review books only Thursday, so the conflict goes away. The task dates are identical in both runs: slices change load, never dates.

How slices behave:

- The slice is clipped to the task's work. `Offset = 3, Duration = 5` on a 4-day task books only the last day, and an `Offset` at or past the task's duration books nothing.
- Clipping happens on every schedule, not on import. If the task gets shorter later, the slice follows it.
- Non-working time and leveling pauses don't count as worked time. A pause or a `LevelingDelay` moves the slice together with the work.
- Two assignments of the same resource on the same task book separately, each at its own `Value`. Where their slices overlap, the load adds up.
- In [`CalendarMode.Resource`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/calendar-mode.md>) a slice is measured on the assignment's calendar. A sliced assignment still counts toward the task's calendar envelope, even when its slice is empty.
- Every load consumer sees the sliced load: [`Overallocations`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#overallocations>), [`LoadProfile`](<https://docs.dhtmlx.com/planning-engine/net/api/project-load-profile.md>), the overload metric of [`Evaluate`](<https://docs.dhtmlx.com/planning-engine/net/api/project-evaluate.md>) and the decisions of [`Level`](<https://docs.dhtmlx.com/planning-engine/net/api/project-level.md>).

In a live project, edit a slice with [`AssignmentUpdate.Offset`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/assignment-update.md>) and `AssignmentUpdate.Duration`. [`Patch<double>.Null`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/patch.md#null>) clears a field: `Offset` goes back to the task start and `Duration` to the task end. Slice edits never change task dates. An invalid value is a rejected operation in [`ApplyReport.Rejected`](<https://docs.dhtmlx.com/planning-engine/net/api/core/changes/apply-report.md#rejected>).

For a step-by-step version, see [Split assignment work](<https://docs.dhtmlx.com/planning-engine/net/how-to/split-assignment-work.md>).

Assignments have no mode. Every resource is checked for overallocation; use slices to shape how work is spread.

## Overallocation windows

A window is a run of contiguous overallocated buckets of one resource. Each [`OverallocationWindow`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/resource-overallocation.md#overallocationwindow>) (see [`ResourceOverallocation`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/resource-overallocation.md>)) has these properties:

| Property | Meaning |
|---|---|
| `From`, `To` | The window edges, clipped to the contending tasks' working time (and to their slices) |
| `Capacity` | The resource's capacity |
| `Peak` | The highest bucket load in the window |
| `Overload` | The sum of `load − capacity` over the window's buckets, in capacity × buckets |
| `Tasks` | The contending task ids, in input order |
| `Reason` | Set only by [`Level`](<https://docs.dhtmlx.com/planning-engine/net/api/project-level.md>): why leveling left this window. See [Leveling resources](<https://docs.dhtmlx.com/planning-engine/net/resources-and-assignment/leveling-resources.md>) |

Non-working time splits windows. Here two 2-day tasks start on a Friday and continue on Monday:

```csharp
using Plen;

// Both tasks start on Friday 2026-08-14 and run 2 working days.
var friday = Moment.FromYmdHm(2026, 8, 14, 8, 0) ?? throw new InvalidOperationException("invalid date");

var data = new ProjectData
{
    Tasks =
    [
        new TaskData("api") { StartDate = friday, Duration = 2 },
        new TaskData("ui") { StartDate = friday, Duration = 2 },
    ],
    Resources = [new ResourceData { Id = "dev" }],
    // No Id: the engine generates one for each assignment.
    Assignments =
    [
        new AssignmentData { Task = "api", Resource = "dev", Value = 1 },
        new AssignmentData { Task = "ui", Resource = "dev", Value = 1 },
    ],
};

var result = Engine.Schedule(data, new SolveOptions());
foreach (var resource in result.Overallocations ?? [])
{
    foreach (var w in resource.Windows)
    {
        Console.WriteLine(
            $"{resource.Resource}: {w.From} -> {w.To} peak={w.Peak} capacity={w.Capacity} " +
            $"overload={w.Overload} tasks=[{string.Join(", ", w.Tasks)}]");
    }
}
```

Output:

```text
dev: 2026-08-14 08:00 -> 2026-08-14 17:00 peak=2 capacity=1 overload=1 tasks=[api, ui]
dev: 2026-08-17 08:00 -> 2026-08-17 17:00 peak=2 capacity=1 overload=1 tasks=[api, ui]
```

The weekend holds no work, so Friday and Monday become two windows. Each one has `Overload` `1`: one day at one unit over capacity. On an hour basis the lunch break splits windows the same way.

What else windows tell you, and what they don't:

- [`Overallocations`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#overallocations>) is `null` when the project has no assignments, and an empty list when there's no contention. A resource appears only when it has at least one window. Resources keep their input order.
- Windows follow the scheduled dates, not the stored ones. If a link pushes `ui` after `api`, the contention is gone and so is the window.
- Detection works in whole buckets, but `From` and `To` are clipped to working time. Two 2-day tasks that start Monday end Wednesday 08:00, yet their window ends Tuesday 17:00, because Wednesday before 08:00 holds no working time.
- Per resource, the windows' `Overload` values add up to the overload [`Evaluate`](<https://docs.dhtmlx.com/planning-engine/net/api/project-evaluate.md>) reports for that resource at the same basis.

For a per-bucket view of load and capacity (a resource histogram), use [`Project.LoadProfile`](<https://docs.dhtmlx.com/planning-engine/net/api/project-load-profile.md>). See [Resource load](<https://docs.dhtmlx.com/planning-engine/net/resources-and-assignment/resource-load.md>).
