[Canonical page](<https://docs.dhtmlx.com/planning-engine/wasm/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. You get the data a resource histogram needs. 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

[`loadProfile`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-load-profile.md>) reads the solved baseline of a [`JsonProject`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md>) and cuts the date range into **buckets** of one hour or one day. For every resource it reports each bucket's `load` and `capacity`. The call is read-only: it never schedules and never changes the project.

The request and the response fit together like this:

- The request is a [`LoadProfileOptions`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-load-profile.md#loadprofileoptions>) object. It says what to measure: the range `from`..`to` (both required), which `resources`, the bucket size ([`basis`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#load-basis>)) and whether to list `tasks`. It also takes an optional `baselineId`. Every field except `from` and `to` can be left out: you then get every resource, the default basis and no task lists.
- The response is a [`LoadProfile`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/load-profile.md>) in the envelope's `result`: the `basis` it used, and one entry per resource with the resource id, its `capacityMode` and a [`buckets`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/load-profile.md#loadbucket>) array. Each bucket is `{ start, load, capacity }`, plus `tasks` when you ask for them.

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, so two full-time tasks on the same day give a load of 2.
- **Same load as leveling.** The profile counts load the same way as leveling and the `overallocations` in [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/schedule-result.md>) do: 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 so do the task ids in a bucket.

The profile always describes the accepted baseline, so the project must be solved and have no unsolved edits. Failures come back as an error envelope:

| Failure | Error envelope |
|---|---|
| Project never solved, or edited with [`apply`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md>) since the last solve | `kind: "project"`, `code: "no-baseline"` |
| `baselineId` doesn't name the current baseline | `kind: "project"`, `code: "stale-baseline"` |
| `to <= from`, or more than 100 000 buckets per resource | `kind: "validation"`, `path: "to"` |
| Unknown id in `resources` | `kind: "validation"`, `path: "resources"` |
| `tasks` isn't `"none"`, `"overallocated"` or `"all"` | `kind: "validation"`, `path: null` |
| Unknown field in the options | `kind: "parse"` |

See [Error handling](<https://docs.dhtmlx.com/planning-engine/wasm/error-handling.md>) for the envelope and the [`LoadProfile` reference](<https://docs.dhtmlx.com/planning-engine/wasm/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/wasm/api/project-load-profile.md>) as JSON. Save this as `profile.cjs`:

```js
const { JsonProject } = require("./pkg-node/plen_wasm.js");

function unwrap(text) {
  const response = JSON.parse(text);
  if (!response.ok) {
    const { kind, code, message, path } = response.error;
    throw new Error(`${kind} ${code ?? ""}: ${message} (at ${path})`);
  }
  return response.result;
}

// 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 JsonProject("{}");
try {
  unwrap(project.solve(JSON.stringify(data), "{}"));

  // Sunday 9 to Thursday 13; `to` is exclusive.
  const query = { from: "2026-08-09 00:00", to: "2026-08-14 00:00" };
  const profile = unwrap(project.loadProfile(JSON.stringify(query)));

  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}`);
    }
  }
} finally {
  project.free();
}
```

Output of `node profile.cjs`:

```text
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 has `a` and `b` (2), Tuesday has `a` plus `c` at half time (1.5), and Wednesday has `d` (1). Capacity is 1 in every bucket: `dev` has no `capacity` of its own, so it takes [`config.default_resource_capacity`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-config.md>).

For a single day, the `result` in the envelope looks like this:

```json
{
  "basis": "day",
  "resources": [
    {
      "buckets": [{ "capacity": 1, "load": 2, "start": "2026-08-10 00:00" }],
      "capacityMode": "flat",
      "resource": "dev"
    }
  ]
}
```

### Narrowing the query

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

```js
const { JsonProject } = require("./pkg-node/plen_wasm.js");

function unwrap(text) {
  const response = JSON.parse(text);
  if (!response.ok) {
    const { kind, code, message, path } = response.error;
    throw new Error(`${kind} ${code ?? ""}: ${message} (at ${path})`);
  }
  return response.result;
}

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 },
  ],
  resources: [{ id: "dev" }, { id: "qa" }],
  assignments: [
    { task: "a", resource: "dev", value: 1 },
    { task: "b", resource: "dev", value: 1 },
  ],
};

const project = new JsonProject("{}");
try {
  unwrap(project.solve(JSON.stringify(data), "{}"));

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

  for (const row of profile.resources) {
    console.log(`${row.resource}: ${row.buckets.map((b) => b.load).join(" ")}`);
  }
} finally {
  project.free();
}
```

Output:

```text
dev: 0 2 2
```

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 fails the call:

```json
{
  "error": { "code": null, "kind": "validation", "message": "unknown resource \"ops\"", "path": "resources" },
  "ok": false
}
```

### 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`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/schedule-result.md#baselineid>) from the solve result in the options. If the project was solved again in the meantime, the call returns a `stale-baseline` error instead of load for other dates.

```js
const { JsonProject } = require("./pkg-node/plen_wasm.js");

function unwrap(text) {
  const response = JSON.parse(text);
  if (!response.ok) {
    const { kind, code, message, path } = response.error;
    throw new Error(`${kind} ${code ?? ""}: ${message} (at ${path})`);
  }
  return response.result;
}

const data = {
  tasks: [{ id: "a", start_date: "2026-08-10 08:00", duration: 2 }],
  resources: [{ id: "dev" }],
  assignments: [{ task: "a", resource: "dev", value: 1 }],
};

const project = new JsonProject("{}");
try {
  const solved = unwrap(project.solve(JSON.stringify(data), "{}"));

  const options = {
    baselineId: solved.baselineId,
    from: "2026-08-10 00:00",
    to: "2026-08-15 00:00",
  };
  const profile = unwrap(project.loadProfile(JSON.stringify(options)));
  console.log(`${profile.resources.length} resource(s) measured`);
} finally {
  project.free();
}
```

`baselineId` sits next to `from` and `to` in the same object. After another solve, even with the same data, the same options give:

```json
{
  "error": { "code": "stale-baseline", "kind": "project", "message": "stale-baseline", "path": null },
  "ok": false
}
```

Take the token from the latest solve result. Tokens are opaque, 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 resource entry tells you how that capacity is spread over the buckets:

- `"calendar"`: the resource has its own `calendar_id`, [`config.calendar_mode`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#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
const { JsonProject } = require("./pkg-node/plen_wasm.js");

function unwrap(text) {
  const response = JSON.parse(text);
  if (!response.ok) {
    const { kind, code, message, path } = response.error;
    throw new Error(`${kind} ${code ?? ""}: ${message} (at ${path})`);
  }
  return response.result;
}

const data = {
  calendars: [
    { id: "global", worktime: { hours: ["8:00-16:00"] } },
    { 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"] } },
  ],
  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 JsonProject("{}");
try {
  unwrap(project.solve(JSON.stringify(data), "{}"));

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

  // Tuesday 06:00 to 18:00 in hour buckets, for the part-time resource.
  const day = unwrap(
    project.loadProfile(
      JSON.stringify({
        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).join(" ")}`);
} finally {
  project.free();
}
```

Output:

```text
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 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.

Add `config: { calendar_mode: "task" }` to the data (or `use_calendars: false`) and all three resources become `"flat"`, with capacity 1 in every bucket. See [Working time and calendars](<https://docs.dhtmlx.com/planning-engine/wasm/scheduling/working-time-calendars.md>) for calendar setup and [`SchedulingConfig`](<https://docs.dhtmlx.com/planning-engine/wasm/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` array:

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

```js
const { JsonProject } = require("./pkg-node/plen_wasm.js");

function unwrap(text) {
  const response = JSON.parse(text);
  if (!response.ok) {
    const { kind, code, message, path } = response.error;
    throw new Error(`${kind} ${code ?? ""}: ${message} (at ${path})`);
  }
  return response.result;
}

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: [
    { 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 },
  ],
};

const project = new JsonProject("{}");
try {
  unwrap(project.solve(JSON.stringify(data), "{}"));

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

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

Output:

```text
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 `"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/wasm/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/wasm/api/types/overallocation.md>)). [Leveling resources](<https://docs.dhtmlx.com/planning-engine/wasm/resources-and-assignment/leveling-resources.md>) shows how to remove the overload.

## Load of hypothetical dates

[`loadProfile`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-load-profile.md>) only reads the baseline. To see the load an edit *would* cause, add a [`profile`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#profilequery>) query to the options of [`preview`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-preview.md>). The result's `profile` is computed on the previewed dates, and the project stays as it was.

```js
const { JsonProject } = require("./pkg-node/plen_wasm.js");

function unwrap(text) {
  const response = JSON.parse(text);
  if (!response.ok) {
    const { kind, code, message, path } = response.error;
    throw new Error(`${kind} ${code ?? ""}: ${message} (at ${path})`);
  }
  return response.result;
}

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 },
  ],
  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 },
  ],
};

const project = new JsonProject("{}");
try {
  const solved = unwrap(project.solve(JSON.stringify(data), "{}"));
  const range = { from: "2026-08-10 00:00", to: "2026-08-12 00:00" };

  // What if `b` moves from `dev` to `qa`?
  const changes = { assignments: { update: [{ id: "b:dev", resource: "qa" }] } };
  const options = { baselineId: solved.baselineId, profile: range };
  const preview = unwrap(project.preview(JSON.stringify(changes), JSON.stringify(options)));

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

  // The baseline still has both tasks on `dev`.
  const base = unwrap(project.loadProfile(JSON.stringify({ ...range, resources: ["dev"] })));
  console.log(`baseline dev: ${base.resources[0].buckets.map((b) => b.load).join(" ")}`);
} finally {
  project.free();
}
```

Output:

```text
previewed dev: 1 1.5
previewed qa: 1 0
baseline dev: 2 1.5
```

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 a `profile` query, the preview result has no `profile` field.

The `profile` query takes the same fields as the `loadProfile` options except `baselineId`. A `baselineId` inside `profile` is an unknown field and fails the preview with a `parse` error. Put it at the top level of the preview options instead, as the example does. An invalid query, such as an unknown resource, fails the whole preview with a `validation` error.

To profile a multi-step edit or a leveling run, [`clone()`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md#clone>) the project, edit or [`level`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-level.md>) the clone, and call `loadProfile` on it. Cloning is cheap and the original stays as it was. Free the clone when you're done. See [Previewing changes](<https://docs.dhtmlx.com/planning-engine/wasm/live-projects/previewing-changes.md>) for both approaches.
