[Canonical page](<https://docs.dhtmlx.com/planning-engine/js/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(options)`](<https://docs.dhtmlx.com/planning-engine/js/api/project-load-profile.md>) reads the solved baseline and cuts the date range into **buckets**, either hours or days. For every resource it reports each bucket's `load` and `capacity`. The call is synchronous and read-only. It never schedules and never changes the project.

The parts fit together like this:

- The options object says what to measure: the range `from`..`to`, which `resources`, the bucket size (`basis`) and whether to list `tasks`. Only `from` and `to` are required. Leave the rest out and you get every resource, the default basis and no task lists. An optional `baselineId` pins the call to a specific baseline.
- The result is a [`LoadProfile`](<https://docs.dhtmlx.com/planning-engine/js/api/types/load-profile.md>): the `basis` it used and one entry per resource in `resources`. Each entry has the `resource` id, its `capacityMode` and a list of `buckets`, each `{ start, load, capacity, tasks? }`.

A few 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.duration_unit`: `"day"` for days, `"hour"` for hours and minutes. Set `basis` to override it.
- **Load units.** Load is in assignment `value` units, where 1 is 100 %. Nothing is converted to hours. An assignment that works in a bucket books its `value` there. Two full-time tasks on the same day give a load of 2.
- **Same load as leveling.** The profile uses the same sweep as the leveler and the `overallocations` in [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/js/api/types/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, and task ids in a bucket too.

Bucket `start` dates follow the project's date setting, like every other result date.

:::note
Set `dates: "date"` to get result dates as local `Date` objects instead of `"YYYY-MM-DD HH:MM"` strings; [`schedule()`](<https://docs.dhtmlx.com/planning-engine/js/api/schedule.md>) always returns strings.
:::

The profile always describes the accepted baseline, so the project has to be solved and clean. Failures are thrown synchronously:

| Failure | Error |
|---|---|
| Project not solved yet, or edited with [`apply`](<https://docs.dhtmlx.com/planning-engine/js/api/project-apply.md>) since the last solve | [`ProjectError`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md#projecterror>), `code: "no-baseline"` |
| `baselineId` doesn't name the current baseline | `ProjectError`, `code: "stale-baseline"` |
| `to <= from`, or more than 100 000 buckets | [`InputError`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md#inputerror>), `path: "to"` |
| Unknown id in `resources` | `InputError`, `path: "resources"` |
| `tasks` isn't `"none"`, `"overallocated"` or `"all"` | `InputError` |

See [Handling errors](<https://docs.dhtmlx.com/planning-engine/js/error-handling.md>) for the error classes and the [`LoadProfile` reference](<https://docs.dhtmlx.com/planning-engine/js/api/types/load-profile.md>) for every field.

## Querying a profile

Solve the project, then pass the range to [`loadProfile`](<https://docs.dhtmlx.com/planning-engine/js/api/project-load-profile.md>).

```js
import { Project } from "plen-js";

// 2026-08-10 is a Monday.
const data = {
  tasks: [
    { id: "a", start_date: "2026-08-10 08:00", duration: 2 },
    { id: "b", start_date: "2026-08-10 08:00", duration: 1 },
    { id: "c", start_date: "2026-08-11 08:00", duration: 1 },
    { id: "d", start_date: "2026-08-12 08:00", duration: 1 },
  ],
  resources: [{ id: "dev" }],
  assignments: [
    { id: "a:dev", task: "a", resource: "dev", value: 1 },
    { id: "b:dev", task: "b", resource: "dev", value: 1 },
    { id: "c:dev", task: "c", resource: "dev", value: 0.5 },
    { id: "d:dev", task: "d", resource: "dev", value: 1 },
  ],
};

const project = new Project();
project.solve(data);

// Sunday 9 up to Friday 14; `to` is exclusive.
const profile = project.loadProfile({ from: "2026-08-09", to: "2026-08-14" });

console.log("basis:", profile.basis);
for (const row of profile.resources) {
  for (const bucket of row.buckets) {
    console.log(`${row.resource} ${bucket.start}: load ${bucket.load} of ${bucket.capacity}`);
  }
}
```

The project uses day units, so the basis is `"day"` and you get five buckets, Sunday to Thursday. The loads are 0, 2 (`a` and `b`), 1.5 (`a` plus `c` at half time), 1 (`d`) and 0. Capacity is 1 in every bucket: `dev` has no `capacity` of its own, so it takes `config.default_resource_capacity`.

:::note
Pass the query fields (`from`, `to`, ...) directly in the options object, next to `baselineId`.
:::

### Narrowing the query

Set `resources` to measure only some resources, and `basis` to change the bucket size.

```js
import { Project } from "plen-js";

const project = new Project();
project.solve({
  tasks: [
    { id: "a", start_date: "2026-08-10 08:00", duration: 2 },
    { id: "b", start_date: "2026-08-10 08:00", duration: 1 },
  ],
  resources: [{ id: "dev" }, { id: "qa" }],
  assignments: [
    { task: "a", resource: "dev", value: 1 },
    { task: "b", resource: "dev", value: 1 },
  ],
});

// Monday 07:00 to 10:00, hour buckets, `dev` only.
const profile = project.loadProfile({
  from: "2026-08-10 07:00",
  to: "2026-08-10 10:00",
  resources: ["dev"],
  basis: "hour",
});

for (const row of profile.resources) {
  console.log(row.resource, row.buckets.map((b) => b.load));
}
```

Only `dev` comes back, with three hour buckets: 07:00 is before work starts (0), and 08:00 and 09:00 carry both tasks (2 each). An id in `resources` that isn't in the project throws an [`InputError`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md#inputerror>) 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 `baselineId` from the solve result. If the project was solved again in the meantime, the call throws a [`ProjectError`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md#projecterror>) with `code: "stale-baseline"` instead of returning load for other dates.

```js
import { Project, ProjectError } from "plen-js";

const project = new Project();
const solved = project.solve({
  tasks: [{ id: "a", start_date: "2026-08-10 08:00", duration: 2 }],
  resources: [{ id: "dev" }],
  assignments: [{ task: "a", resource: "dev", value: 1 }],
});

try {
  const profile = project.loadProfile({
    from: "2026-08-10",
    to: "2026-08-15",
    baselineId: solved.baselineId,
  });
  console.log(`${profile.resources.length} resource(s) measured`);
} catch (e) {
  if (e instanceof ProjectError && e.code === "stale-baseline") {
    // The plan changed since `solved`: re-read the dates, then the profile.
  } else {
    throw e;
  }
}
```

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` (default `config.default_resource_capacity`, 1). The `capacityMode` of each entry in `profile.resources` tells you how it's spread over the buckets:

- `"calendar"`: the resource has its own calendar, `config.calendar_mode` is `"resource"` (the default) and `config.use_calendars` is on. Buckets with working time on that calendar get the full capacity, all others get 0.
- `"flat"`: every bucket gets the full capacity, weekends and nights included. This applies when the resource has no calendar, when `calendar_mode` is `"task"` or `"task-inherited"`, or when `use_calendars` 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. So a bucket with capacity 0 never gets load, and a bucket overallocates in the profile exactly when leveling sees it as overallocated.

```js
import { Project } from "plen-js";

// `days` runs Sunday..Saturday.
const data = {
  calendars: [
    { id: "global", worktime: { hours: ["8:00-16:00"], days: [0, 1, 1, 1, 1, 1, 0] } },
    { id: "tue_sat", worktime: { hours: ["8:00-16:00"], days: [0, 0, 1, 1, 1, 1, 1] } },
    { id: "mornings", worktime: { hours: ["8:00-12:00"], days: [0, 1, 1, 1, 1, 1, 0] } },
  ],
  tasks: [{ id: "a", start_date: "2026-08-11 08:00", duration: 2 }], // Tuesday
  resources: [
    { id: "office" },
    { id: "shop", calendar_id: "tue_sat" },
    { id: "part_time", calendar_id: "mornings" },
  ],
  assignments: [{ task: "a", resource: "shop", value: 1 }],
};

const project = new Project();
project.solve(data);

// One week in day buckets, Sunday 9 to Saturday 15.
const week = project.loadProfile({ from: "2026-08-09", to: "2026-08-16" });
for (const row of week.resources) {
  console.log(
    `${row.resource} (${row.capacityMode})`,
    "capacity", row.buckets.map((b) => b.capacity),
    "load", row.buckets.map((b) => b.load),
  );
}

// Tuesday 06:00 to 18:00 in hour buckets, for the part-time resource.
const day = project.loadProfile({
  from: "2026-08-11 06:00",
  to: "2026-08-11 18:00",
  resources: ["part_time"],
  basis: "hour",
});
console.log("part_time hourly capacity", day.resources[0].buckets.map((b) => b.capacity));
```

What the week shows:

- `office` has no calendar, so it's `"flat"` with capacity 1 on all seven days, Sunday and Saturday included.
- `shop` is `"calendar"`. Its capacity is 0 on Sunday and Monday and 1 from Tuesday to Saturday. Task `a` runs Tuesday and Wednesday, so those two buckets have load 1.
- 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 `config: { calendar_mode: "task" }` (or `use_calendars: false`) in the data and all three resources become `"flat"`, with capacity 1 in every bucket. See [Working time and calendars](<https://docs.dhtmlx.com/planning-engine/js/scheduling/working-time-calendars.md>) for calendar setup and [`SchedulingConfig`](<https://docs.dhtmlx.com/planning-engine/js/api/types/scheduling-config.md>) for the config fields.

## Task lists per bucket

A histogram usually needs to say *who* is overloading a resource. Set `tasks` in the options to fill each bucket's `tasks`:

| `tasks` option | Bucket `tasks` |
|---|---|
| `"none"` (default) | absent in every bucket |
| `"overallocated"` | task ids in buckets where `load > capacity`, `[]` elsewhere |
| `"all"` | ids of every task booked in the bucket |

```js
import { Project } from "plen-js";

const project = new Project();
project.solve({
  tasks: [
    { id: "a", start_date: "2026-08-10 08:00", duration: 2 },
    { id: "b", start_date: "2026-08-10 08:00", duration: 1 },
    { id: "c", start_date: "2026-08-11 08:00", duration: 1 },
    { id: "d", start_date: "2026-08-12 08:00", duration: 1 },
  ],
  resources: [{ id: "dev" }],
  assignments: [
    { task: "a", resource: "dev", value: 1 },
    { task: "b", resource: "dev", value: 1 },
    { task: "c", resource: "dev", value: 0.5 },
    { task: "d", resource: "dev", value: 1 },
  ],
});

// Monday 10 to Thursday 13.
const profile = project.loadProfile({
  from: "2026-08-10",
  to: "2026-08-14",
  tasks: "overallocated",
});

for (const bucket of profile.resources[0].buckets) {
  console.log(`${bucket.start}: load ${bucket.load}, tasks`, bucket.tasks);
}
```

With `"overallocated"`, Monday lists `a` and `b`, and Tuesday lists `a` and `c`. Wednesday and Thursday aren't overloaded, so they get empty arrays. Switch to `"all"` and Wednesday lists `d` too, because `"all"` reports every booked task regardless of capacity.

If you need only the overloaded periods, not the full histogram, skip the profile. Every [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/js/api/types/schedule-result.md>) of a project with assignments carries `overallocations`: per-resource windows with `peak`, `overload` and the contending `tasks` (see [`Overallocation`](<https://docs.dhtmlx.com/planning-engine/js/api/types/overallocation.md>)). See [Leveling resources](<https://docs.dhtmlx.com/planning-engine/js/resources-and-assignment/leveling-resources.md>) for how to remove the overload.

## Load of hypothetical dates

[`loadProfile`](<https://docs.dhtmlx.com/planning-engine/js/api/project-load-profile.md>) only reads the baseline. To see the load an edit *would* cause, set the `profile` option of [`preview`](<https://docs.dhtmlx.com/planning-engine/js/api/project-preview.md>) to a query. The result's `profile` is computed on the previewed dates, and the project is left untouched.

```js
import { Project } from "plen-js";

const project = new Project();
project.solve({
  tasks: [
    { id: "a", start_date: "2026-08-10 08:00", duration: 2 },
    { id: "b", start_date: "2026-08-10 08:00", duration: 1 },
    { id: "c", start_date: "2026-08-11 08:00", duration: 1 },
  ],
  resources: [{ id: "dev" }, { id: "qa" }],
  assignments: [
    { id: "a:dev", task: "a", resource: "dev", value: 1 },
    { id: "b:dev", task: "b", resource: "dev", value: 1 },
    { id: "c:dev", task: "c", resource: "dev", value: 0.5 },
  ],
});

// What if `b` moves from `dev` to `qa`?
const changes = { assignments: { update: [{ id: "b:dev", resource: "qa" }] } };
const range = { from: "2026-08-10", to: "2026-08-12" };

const preview = project.preview(changes, { profile: range });
for (const row of preview.profile.resources) {
  console.log(`previewed ${row.resource}`, row.buckets.map((b) => b.load));
}

// The baseline still has both tasks on `dev`.
const base = project.loadProfile(range);
console.log("baseline dev", base.resources[0].buckets.map((b) => b.load));
```

In the preview, `dev` drops to 1 on Monday and keeps 1.5 on Tuesday, and `qa` gets 1 on Monday. The baseline profile still shows 2 and 1.5 for `dev`. Without the `profile` option, the preview result has no `profile` field.

The `profile` option takes the query fields only (`from`, `to`, `resources`, `basis`, `tasks`). A `baselineId` inside it is an unknown field and throws an [`InputError`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md#inputerror>). To guard a preview against a stale baseline, set `baselineId` on the preview options instead. An invalid query fails the whole preview with an `InputError`.

To profile a multi-step edit or a leveling run, clone the project with [`clone()`](<https://docs.dhtmlx.com/planning-engine/js/api/project.md#clone>), edit or [`level`](<https://docs.dhtmlx.com/planning-engine/js/api/project-level.md>) the clone, and call `loadProfile` on it. The original stays as it was. See [Previewing changes](<https://docs.dhtmlx.com/planning-engine/js/live-projects/previewing-changes.md>) for both approaches.

:::note
`clone()` copies the whole project at once, so its cost grows with the project's size.
:::
