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


# Resource load

This guide shows how to measure how busy each resource is over a date range. That's the data behind a resource histogram. It covers the profile query, how capacity follows resource calendars, which tasks fill each bucket, and how to get the load for dates you haven't committed yet.

## How a load profile works

[`Project.LoadProfile`](<https://docs.dhtmlx.com/planning-engine/net/api/project-load-profile.md>) reads the solved baseline and cuts the date range into **buckets** of one hour or one day. For every resource it reports each bucket's `Load` and `Capacity`. It's read-only: it never schedules and never changes the project.

The parts fit together like this:

- A [`ProfileQuery`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/profile-query.md>) says what to measure: the range `From`..`To`, which `Resources`, the bucket size (`Basis`) and whether to list `Tasks`. `new ProfileQuery(from, to)` gives you every resource, the default basis and no task lists. Set the other properties with an object initializer.
- [`LoadProfileOptions`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/load-profile-options.md>) wraps the query and adds an optional [`BaselineId`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/load-profile-options.md#baselineid>). A `ProfileQuery` converts to `LoadProfileOptions` implicitly, so `project.LoadProfile(query)` is the short form.
- The result is a [`LoadProfile`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/load-profile.md>): the `Basis` it used and one [`ResourceLoad`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/load-profile.md#resourceload>) per resource. Each `ResourceLoad` has the `Resource` id, its [`CapacityMode`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/load-profile.md#capacitymode>) and a list of [`LoadBucket`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/load-profile.md#loadbucket>) records with `Start`, `Load`, `Capacity` and `Tasks`.

These rules decide what ends up in the buckets:

- **Range.** A bucket is included when its start lies in `[From, To)`. A day bucket starts at midnight, so a `From` of Monday 12:00 skips Monday and starts on Tuesday. `To` is exclusive.
- **Dense output.** Every bucket in the range appears, including empty ones with load 0. That's why the range is capped at 100 000 buckets per resource, about 11 years at hour basis.
- **Basis.** By default the basis follows [`config.DurationUnit`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#durationunit>): [`Basis.Day`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/basis.md>) for days, `Basis.Hour` for hours and minutes. Set `Basis` to override it.
- **Load units.** Load is in assignment [`Value`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/assignment-data.md#value>) units, where 1 is 100 %. Nothing is converted to hours. An assignment that works in a bucket books its `Value` there, so two full-time tasks on the same day give a load of 2.0.
- **Same load as leveling.** The profile uses the same sweep as the leveler and the [`Overallocations`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#overallocations>) in [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md>): only real work segments, each assignment on its own calendar, cut to its slice. A bucket overallocates when `Load > Capacity`.
- **Order.** Resources come back in input order. Task ids in a bucket do too.

The profile always describes the accepted baseline, so the project has to be solved and clean.

| Failure | Exception |
|---|---|
| Project not solved yet, or edited with [`Apply`](<https://docs.dhtmlx.com/planning-engine/net/api/project-apply.md>) since the last solve | [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/project-exception.md>), `Code` = [`ProjectError.NoBaseline`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/project-exception.md#projecterror>) |
| `BaselineId` doesn't name the current baseline | `ProjectException`, `Code` = `ProjectError.StaleBaseline` |
| `To <= From`, or more than 100 000 buckets | [`InputException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/input-exception.md>), `Path` = `"to"` |
| Unknown id in `Resources` | `InputException`, `Path` = `"resources"` |

See [Error handling](<https://docs.dhtmlx.com/planning-engine/net/error-handling.md>) for the exception types and the [`LoadProfile` reference](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/load-profile.md>) for every property.

## Querying a profile

Solve the project, build a [`ProfileQuery`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/profile-query.md>) and pass it to [`LoadProfile`](<https://docs.dhtmlx.com/planning-engine/net/api/project-load-profile.md>).

```csharp
using Plen;

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

static TaskData Task(string id, Moment start, double duration) =>
    new(id) { StartDate = start, Duration = duration };

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

// 2026-08-10 is a Monday.
var data = new ProjectData
{
    Tasks =
    [
        Task("a", At(10, 8), 2.0),
        Task("b", At(10, 8), 1.0),
        Task("c", At(11, 8), 1.0),
        Task("d", At(12, 8), 1.0),
    ],
    Resources = [new ResourceData { Id = "dev" }],
    Assignments =
    [
        Assign("a", "dev", 1.0),
        Assign("b", "dev", 1.0),
        Assign("c", "dev", 0.5),
        Assign("d", "dev", 1.0),
    ],
};

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

// Sunday 9 up to Friday 14; `To` is exclusive.
var profile = project.LoadProfile(new ProfileQuery(At(9, 0), At(14, 0)));

Console.WriteLine($"basis: {profile.Basis}");
foreach (var row in profile.Resources)
{
    foreach (var bucket in row.Buckets)
        Console.WriteLine($"{row.Resource} {bucket.Start}: load {bucket.Load} of {bucket.Capacity}");
}
```

Output:

```
basis: Day
dev 2026-08-09 00:00: load 0 of 1
dev 2026-08-10 00:00: load 2 of 1
dev 2026-08-11 00:00: load 1.5 of 1
dev 2026-08-12 00:00: load 1 of 1
dev 2026-08-13 00:00: load 0 of 1
```

The project uses day units, so the basis is `Day` and you get five buckets, Sunday to Thursday. Monday carries `a` and `b`, and Tuesday carries `a` plus `c` at half time. Capacity is 1.0 in every bucket: `dev` has no capacity of its own, so it takes [`config.DefaultResourceCapacity`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#defaultresourcecapacity>).

### Narrowing the query

Set `Resources` to measure only some resources, and [`Basis`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/basis.md>) to change the bucket size.

```csharp
using Plen;

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

static TaskData Task(string id, Moment start, double duration) =>
    new(id) { StartDate = start, Duration = duration };

var data = new ProjectData
{
    Tasks = [Task("a", At(10, 8), 2.0), Task("b", At(10, 8), 1.0)],
    Resources = [new ResourceData { Id = "dev" }, new ResourceData { Id = "qa" }],
    Assignments =
    [
        new AssignmentData { Task = "a", Resource = "dev", Value = 1.0 },
        new AssignmentData { Task = "b", Resource = "dev", Value = 1.0 },
    ],
};

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

// Monday 07:00 to 10:00, hour buckets, `dev` only.
var query = new ProfileQuery(At(10, 7), At(10, 10))
{
    Resources = ["dev"],
    Basis = Basis.Hour,
};
var profile = project.LoadProfile(query);

foreach (var row in profile.Resources)
    Console.WriteLine($"{row.Resource}: [{string.Join(", ", row.Buckets.Select(b => b.Load))}]");
```

Output:

```
dev: [0, 2, 2]
```

Only `dev` comes back, with three hour buckets. At 07:00 work hasn't started yet, and the 08:00 and 09:00 buckets carry both tasks. An id in `Resources` that isn't in the project throws [`InputException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/input-exception.md>) with `Path` = `"resources"`.

### Pinning the profile to a baseline

A UI often fetches the profile some time after the solve. To make sure the profile matches the dates on screen, pass the token from the solve result as [`LoadProfileOptions.BaselineId`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/load-profile-options.md#baselineid>). If the project was solved again in the meantime, the call throws [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/project-exception.md>) with [`ProjectError.StaleBaseline`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/project-exception.md#projecterror>) instead of returning load for other dates.

```csharp
using Plen;

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

var data = new ProjectData
{
    Tasks = [new TaskData("a") { StartDate = At(10, 8), Duration = 2.0 }],
    Resources = [new ResourceData { Id = "dev" }],
    Assignments = [new AssignmentData { Task = "a", Resource = "dev", Value = 1.0 }],
};

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

var options = new LoadProfileOptions(new ProfileQuery(At(10, 0), At(15, 0)))
{
    BaselineId = solved.BaselineId,
};
var profile = project.LoadProfile(options);
Console.WriteLine($"{profile.Resources.Count} resource(s) measured");
```

Take the token from the latest solve result. Tokens are opaque, and the same data solved twice can get a different one, so don't store them or compare them across runs.

## Capacity modes

Capacity is the resource's `Capacity`, or [`config.DefaultResourceCapacity`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#defaultresourcecapacity>) (1.0) when it's `null`. The [`CapacityMode`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/load-profile.md#capacitymode>) of each [`ResourceLoad`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/load-profile.md#resourceload>) tells you how that capacity is spread over the buckets:

- `CapacityMode.Calendar`: the resource has its own calendar, [`config.CalendarMode`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#calendarmode>) is [`CalendarMode.Resource`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/calendar-mode.md>) (the default) and [`config.UseCalendars`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#usecalendars>) is `true`. Buckets with working time on that calendar get the full capacity. All others get 0.
- `CapacityMode.Flat`: every bucket gets the full capacity, weekends and nights included. This applies when the resource has no calendar, when `CalendarMode` is a task mode, or when `UseCalendars` is `false`.

In calendar mode an assignment always works on the resource's calendar, or on the overlap of the task's and the resource's calendars. A bucket with capacity 0 never gets load. A bucket overallocates in the profile exactly when leveling sees it as overallocated.

```csharp
using Plen;

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

// Days run Sunday..Saturday; hours are whole hours.
static CalendarData Calendar(string id, bool[] days, int from, int to) => new()
{
    Id = id,
    Worktime = new Worktime
    {
        Hours = [new TimeRange((ushort)(from * 60), (ushort)(to * 60))],
        Days = days,
    },
};

bool[] monFri = [false, true, true, true, true, true, false];
bool[] tueSat = [false, false, true, true, true, true, true];

var data = new ProjectData
{
    // Tuesday, two days.
    Tasks = [new TaskData("a") { StartDate = At(11, 8), Duration = 2.0 }],
    Calendars =
    [
        Calendar("global", monFri, 8, 16),
        Calendar("tue_sat", tueSat, 8, 16),
        Calendar("mornings", monFri, 8, 12),
    ],
    Resources =
    [
        new ResourceData { Id = "office" },
        new ResourceData { Id = "shop", CalendarId = "tue_sat" },
        new ResourceData { Id = "part_time", CalendarId = "mornings" },
    ],
    Assignments = [new AssignmentData { Task = "a", Resource = "shop", Value = 1.0 }],
};

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

// One week in day buckets, Sunday 9 to Saturday 15.
var week = project.LoadProfile(new ProfileQuery(At(9, 0), At(16, 0)));
foreach (var row in week.Resources)
{
    var capacity = string.Join(", ", row.Buckets.Select(b => b.Capacity));
    var load = string.Join(", ", row.Buckets.Select(b => b.Load));
    Console.WriteLine($"{row.Resource} ({row.CapacityMode}): capacity [{capacity}], load [{load}]");
}

// Tuesday 06:00 to 18:00 in hour buckets, for the part-time resource.
var day = new ProfileQuery(At(11, 6), At(11, 18))
{
    Resources = ["part_time"],
    Basis = Basis.Hour,
};
var hours = project.LoadProfile(day);
Console.WriteLine($"part_time hourly capacity: [{string.Join(", ", hours.Resources[0].Buckets.Select(b => b.Capacity))}]");
```

Output:

```
office (Flat): capacity [1, 1, 1, 1, 1, 1, 1], load [0, 0, 0, 0, 0, 0, 0]
shop (Calendar): capacity [0, 0, 1, 1, 1, 1, 1], load [0, 0, 1, 1, 0, 0, 0]
part_time (Calendar): capacity [0, 1, 1, 1, 1, 1, 0], load [0, 0, 0, 0, 0, 0, 0]
part_time hourly capacity: [0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0]
```

What the week shows:

- `office` has no calendar, so it's `Flat`, with capacity 1.0 on all seven days, Sunday and Saturday included.
- `shop` is `Calendar`. Its capacity is 0 on Sunday and Monday and 1.0 from Tuesday to Saturday. Task `a` runs Tuesday and Wednesday, so those two buckets have load 1.0.
- At hour basis, `part_time` has capacity only in the four hour buckets between 08:00 and 12:00. The other eight hours are 0.

Set `data.Config.CalendarMode = CalendarMode.Task` (or `UseCalendars = false`) and all three resources become `Flat`, with capacity 1.0 in every bucket. See [Working time and calendars](<https://docs.dhtmlx.com/planning-engine/net/scheduling/working-time-calendars.md>) for calendar setup and the [`LoadProfile` reference](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/load-profile.md>) for `CapacityMode`.

## Task lists per bucket

A histogram usually needs to say *who* is overloading a resource. Set [`ProfileQuery.Tasks`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/profile-query.md>) to fill [`LoadBucket.Tasks`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/load-profile.md#loadbucket>):

| [`ProfileTasks`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/profile-tasks.md>) | `LoadBucket.Tasks` |
|---|---|
| `None` (default) | `null` in every bucket |
| `Over` | the task ids in buckets where `Load > Capacity`, an empty list elsewhere |
| `All` | the ids of every task booked in the bucket |

```csharp
using Plen;

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

static TaskData Task(string id, Moment start, double duration) =>
    new(id) { StartDate = start, Duration = duration };

static AssignmentData Assign(string task, double value) =>
    new() { Task = task, Resource = "dev", Value = value };

var data = new ProjectData
{
    Tasks =
    [
        Task("a", At(10, 8), 2.0),
        Task("b", At(10, 8), 1.0),
        Task("c", At(11, 8), 1.0),
        Task("d", At(12, 8), 1.0),
    ],
    Resources = [new ResourceData { Id = "dev" }],
    Assignments = [Assign("a", 1.0), Assign("b", 1.0), Assign("c", 0.5), Assign("d", 1.0)],
};

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

// Monday 10 to Thursday 13.
var query = new ProfileQuery(At(10, 0), At(14, 0)) { Tasks = ProfileTasks.Over };
var profile = project.LoadProfile(query);

foreach (var bucket in profile.Resources[0].Buckets)
{
    var tasks = bucket.Tasks is null ? "null" : $"[{string.Join(", ", bucket.Tasks)}]";
    Console.WriteLine($"{bucket.Start}: load {bucket.Load}, tasks {tasks}");
}
```

Output:

```
2026-08-10 00:00: load 2, tasks [a, b]
2026-08-11 00:00: load 1.5, tasks [a, c]
2026-08-12 00:00: load 1, tasks []
2026-08-13 00:00: load 0, tasks []
```

With `Over`, Monday lists `a` and `b`, and Tuesday lists `a` and `c`. Wednesday and Thursday aren't overloaded, so their lists are empty. Switch to `ProfileTasks.All` and Wednesday lists `d` too, because `All` reports every booked task regardless of capacity.

If you need only the overloaded periods and not the full histogram, skip the profile. Every [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md>) of a project with assignments carries [`Overallocations`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#overallocations>): per-resource windows with `Peak`, `Overload` and the contending `Tasks`. See [Leveling resources](<https://docs.dhtmlx.com/planning-engine/net/resources-and-assignment/leveling-resources.md>) for how to remove the overload.

## Load of hypothetical dates

[`LoadProfile`](<https://docs.dhtmlx.com/planning-engine/net/api/project-load-profile.md>) only reads the baseline. To see the load an edit *would* cause, set [`PreviewOptions.Profile`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/preview-options.md#profile>) to a [`ProfileQuery`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/profile-query.md>) and call [`Project.Preview`](<https://docs.dhtmlx.com/planning-engine/net/api/project-preview.md>). The result's [`Profile`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#profile>) is computed on the previewed dates, and the project stays as it was.

```csharp
using Plen;

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

static TaskData Task(string id, Moment start, double duration) =>
    new(id) { StartDate = start, Duration = duration };

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

var data = new ProjectData
{
    Tasks =
    [
        Task("a", At(10, 8), 2.0),
        Task("b", At(10, 8), 1.0),
        Task("c", At(11, 8), 1.0),
    ],
    Resources = [new ResourceData { Id = "dev" }, new ResourceData { Id = "qa" }],
    Assignments =
    [
        Assign("a", "dev", 1.0),
        Assign("b", "dev", 1.0),
        Assign("c", "dev", 0.5),
    ],
};

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

// What if `b` moves from `dev` to `qa`?
var changes = new SchedulingChanges();
changes.Assignments.Update.Add(new AssignmentUpdate("b:dev") { Resource = Patch<Id>.Of("qa") });

var options = new PreviewOptions { Profile = new ProfileQuery(At(10, 0), At(12, 0)) };
var preview = project.Preview(changes, options);

if (preview.Profile is { } profile)
{
    foreach (var row in profile.Resources)
        Console.WriteLine($"previewed {row.Resource}: [{string.Join(", ", row.Buckets.Select(b => b.Load))}]");
}

// The baseline still has both tasks on `dev`.
var baseline = project.LoadProfile(new ProfileQuery(At(10, 0), At(12, 0)));
Console.WriteLine($"baseline dev: [{string.Join(", ", baseline.Resources[0].Buckets.Select(b => b.Load))}]");
```

Output:

```
previewed dev: [1, 1.5]
previewed qa: [1, 0]
baseline dev: [2, 1.5]
```

In the preview, `dev` drops to 1.0 on Monday and keeps 1.5 on Tuesday, and `qa` gets 1.0 on Monday. The baseline profile still shows 2.0 and 1.5 for `dev`. Without `PreviewOptions.Profile`, `preview.Profile` is `null`.

`PreviewOptions.Profile` takes a bare `ProfileQuery`, not [`LoadProfileOptions`](<https://docs.dhtmlx.com/planning-engine/net/api/core/resources/load-profile-options.md>), so it has no `BaselineId` of its own. To guard a preview against a stale baseline, set [`PreviewOptions.BaselineId`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/preview-options.md#baselineid>). An invalid query fails the whole preview with [`InputException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/input-exception.md>).

To profile a multi-step edit or a leveling run, call [`Clone()`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/project.md#clone>) on the project, edit or [`Level`](<https://docs.dhtmlx.com/planning-engine/net/api/project-level.md>) the clone, and call `LoadProfile` on it. The clone is cheap, and the original doesn't change. See [Previewing changes](<https://docs.dhtmlx.com/planning-engine/net/live-projects/previewing-changes.md>) for both approaches.
