[Canonical page](<https://docs.dhtmlx.com/planning-engine/wasm/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 kinds of records describe staffing. They all go in the [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/project-data.md>) JSON next to the tasks:

- A **resource** (`resources[]`, see [`ResourceData`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#resourcedata>)) is a person, a team or a machine. It has an `id`, an optional `capacity` and an optional `calendar_id`.
- An **assignment** (`assignments[]`, see [`AssignmentData`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/assignment-data.md>)) books one resource on one task at a utilization rate, `value`. `1` means 100 %, so `0.5` is half of someone's time. `task`, `resource` and `value` are required. `task_id` and `resource_id` are accepted as aliases. When both spellings are present, `task` and `resource` win.
- A **slice** is an optional `offset` / `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 without a capacity (or with `"capacity": null`) gets [`config.default_resource_capacity`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-config.md>), which is `1` by default.

To check load, the engine cuts the timeline into **buckets**, hours or days. The basis comes from `config.duration_unit`: days for `"day"`, 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` 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. Assignments only decide where load lands. Calendars are the one exception. In the default `"resource"` calendar mode, 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/wasm/scheduling/working-time-calendars.md#resource-calendars>).

When the project has assignments, every schedule result carries `overallocations`: one entry per contended resource, each with a list of windows (see [`Overallocation`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/overallocation.md>)). Overallocation is data in an `ok: true` result, not an error. Removing it is the job of [leveling](<https://docs.dhtmlx.com/planning-engine/wasm/resources-and-assignment/leveling-resources.md>).

[`schedule`](<https://docs.dhtmlx.com/planning-engine/wasm/api/schedule.md>) and [`solve`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>) with data check resources and assignments. These cases return a `validation` error envelope with the field path:

| Problem | `path` |
|---|---|
| A duplicate resource id | `resources[i].id` |
| A `capacity` below `0` | `resources[i].capacity` |
| A duplicate assignment id | `assignments[i].id` |
| An assignment `value` of `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`, `duration` of `0` or less | `assignments[i].offset`, `assignments[i].duration` |

An assignment without `value` (or without `task` / `resource` under either spelling) is a `parse` error with `path: null`. For example:

```json
{
  "error": { "code": null, "kind": "validation", "message": "unknown resource \"x\"", "path": "assignments[0].resource" },
  "ok": false
}
```

A project created with [`new JsonProject('{"validation":"repair"}')`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md#creating>) clears an invalid slice instead and adds a `repaired` note, so the assignment books the whole task:

```json
{ "kind": "repaired", "message": "invalid assignment offset cleared", "path": "assignments[0].offset" }
```

See [Error handling](<https://docs.dhtmlx.com/planning-engine/wasm/error-handling.md>) for how to read both envelopes.

## 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:

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

const data = {
  // Two 2-day tasks, both starting Monday 2026-08-10.
  tasks: [
    { id: "api", start_date: "2026-08-10 08:00", duration: 2 },
    { id: "ui", start_date: "2026-08-10 08:00", duration: 2 },
  ],
  resources: [
    // One developer: capacity falls back to
    // config.default_resource_capacity (1).
    { id: "dev" },
    // A QA team of two people.
    { id: "qa", capacity: 2 },
  ],
  assignments: [
    { id: "api:dev", task: "api", resource: "dev", value: 1 },
    { id: "ui:dev", task: "ui", resource: "dev", value: 1 },
    { id: "api:qa", task: "api", resource: "qa", value: 1 },
    { id: "ui:qa", task: "ui", resource: "qa", value: 1 },
  ],
};

const response = JSON.parse(schedule(JSON.stringify(data), "{}"));
if (!response.ok) {
  throw new Error(`${response.error.kind}: ${response.error.message} (at ${response.error.path})`);
}
for (const { resource, windows = [] } of response.result.overallocations ?? []) {
  for (const w of windows) {
    console.log(`${resource}: ${w.from} -> ${w.to}, peak ${w.peak} of ${w.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`. `qa` sits exactly at its capacity, which isn't overallocation.

To change the default for every resource without a `capacity`, set `config.default_resource_capacity` (see [`SchedulingConfig`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-config.md>)). With `8`, for example, a `value` of `4` is half of a resource. A resource with its own `capacity` ignores the default.

On a [`JsonProject`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md>), change capacity with an entry in `resources.update` of a [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/scheduling-changes.md>) payload. `"capacity": null` goes back to the default:

```json
{ "resources": { "update": [{ "id": "qa", "capacity": null }] } }
```

Removing a resource also removes all its assignments. [`apply`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md>) lists them in `effects.removedAssignments` of its [`ApplyReport`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md#applyreport>).

## 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:

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

function call(json) {
  const response = JSON.parse(json);
  if (!response.ok) {
    throw new Error(`${response.error.kind}: ${response.error.message} (at ${response.error.path})`);
  }
  return response.result;
}

function report(label, result) {
  const busy = (result.overallocations ?? []).map((r) => r.resource);
  console.log(`${label}: overallocated ${JSON.stringify(busy)}`);
}

const data = {
  tasks: [
    { id: "api", start_date: "2026-08-10 08:00", duration: 2 },
    { id: "ui", start_date: "2026-08-10 08:00", duration: 2 },
  ],
  resources: [{ id: "ana" }, { id: "ben" }, { id: "lead" }],
  assignments: [
    { id: "api:ana", task: "api", resource: "ana", value: 1 },
    { id: "ui:ana", task: "ui", resource: "ana", value: 1 },
    // The lead spends half of their time on each task.
    { id: "api:lead", task: "api", resource: "lead", value: 0.5 },
    { id: "ui:lead", task: "ui", resource: "lead", value: 0.5 },
  ],
};

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

  // Move the UI work from Ana to Ben.
  const changes = { assignments: { update: [{ id: "ui:ana", resource: "ben" }] } };
  const applied = call(project.apply(JSON.stringify(changes)));
  if (applied.rejected.length > 0) {
    const [first] = applied.rejected;
    throw new Error(`${first.path}: ${first.message}`);
  }

  report("after reassign", call(project.solve(undefined, "{}")));
} finally {
  project.free();
}
```

Output:

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

The lead's two half-time assignments add up to `1`, so only Ana shows up at first. The `assignments.update` entry changes the resource of `ui:ana`. 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/wasm/api/types/scheduling-changes.md>) payload:

- `assignments.add` takes [`AssignmentData`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/assignment-data.md>) records. Leave out `id` (or send `null`) and the engine generates one. [`apply`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md>) reports it in `effects.generatedAssignments`.
- `assignments.update` can change `task`, `resource`, `value`, `offset` and `duration`. Fields you leave out stay as they are. `null` on `task` or `resource` is rejected.
- `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's `notes` carry a `dropped-assignment` entry:

```json
{ "assignment": "x", "kind": "dropped-assignment", "reason": "task \"a\" is unscheduled" }
```

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 a reviewer who joins on the last day. Both are **worked time** in the task's `duration_unit`, measured on the assignment's calendar:

- `offset`: where the slice starts, counted from the task start. Absent or `null` means `0`.
- `duration`: how much work the slice books. Absent or `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:

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

function project(reviewSlice) {
  return {
    tasks: [
      // build: 4 working days, Monday to Thursday.
      { id: "build", start_date: "2026-08-10 08:00", duration: 4 },
      // plan: Monday only.
      { id: "plan", start_date: "2026-08-10 08:00", duration: 1 },
    ],
    resources: [{ id: "dev" }, { id: "lead" }],
    assignments: [
      { id: "build:dev", task: "build", resource: "dev", value: 1 },
      // The lead reviews build: the whole task, or only a slice of it.
      { id: "build:lead", task: "build", resource: "lead", value: 1, ...reviewSlice },
      { id: "plan:lead", task: "plan", resource: "lead", value: 1 },
    ],
  };
}

// Whole task, then the last working day only (offset 3 days, 1 day long).
for (const slice of [{}, { offset: 3, duration: 1 }]) {
  const response = JSON.parse(
    schedule(JSON.stringify(project(slice)), '{"includeUnchanged":true}'),
  );
  if (!response.ok) {
    throw new Error(`${response.error.kind}: ${response.error.message} (at ${response.error.path})`);
  }
  console.log(`slice ${JSON.stringify(slice)}`);
  for (const update of response.result.updates) {
    console.log(`  ${update.id}: ${update.start_date} -> ${update.end_date}`);
  }
  for (const { resource, windows = [] } of response.result.overallocations ?? []) {
    for (const w of windows) {
      console.log(`  ${resource} overallocated: ${w.from} -> ${w.to}`);
    }
  }
}
```

Output:

```text
slice {}
  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
slice {"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 `leveling_delay` 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 the `"resource"` calendar mode 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`, [`loadProfile`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-load-profile.md>), the overload metric of [`evaluate`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-evaluate.md>) and the decisions of [`level`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-level.md>).

On a [`JsonProject`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md>), edit a slice with `offset` and `duration` in an `assignments.update` entry. `null` clears a field: `offset` goes back to the task start and `duration` to the task end. Slice edits never change task dates.

```json
{ "assignments": { "update": [{ "id": "build:lead", "offset": null, "duration": null }] } }
```

An invalid value is a rejected operation in the [`ApplyReport`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-apply.md#applyreport>), at the operation's path (`assignments.update[0]`), not an error envelope. Like any rejection, it sets `needsResync`. See [Recover from rejected changes](<https://docs.dhtmlx.com/planning-engine/wasm/how-to/recover-from-rejected-changes.md>).

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

Assignments have no mode. Every resource is checked for overallocation. A `mode` field on an assignment record is an unknown field, so the engine ignores it. Use slices to vary an assignment's value over time.

## Overallocation windows

A window is a run of contiguous overallocated buckets of one resource. Each window in [`overallocations[].windows`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/overallocation.md#window-fields>) has these fields:

| Field | 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/wasm/api/project-level.md>): why leveling left this window. See [Leveling resources](<https://docs.dhtmlx.com/planning-engine/wasm/resources-and-assignment/leveling-resources.md>) |

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

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

// Both tasks start on Friday 2026-08-14 and run 2 working days.
const data = {
  tasks: [
    { id: "api", start_date: "2026-08-14 08:00", duration: 2 },
    { id: "ui", start_date: "2026-08-14 08:00", duration: 2 },
  ],
  resources: [{ id: "dev" }],
  assignments: [
    { task: "api", resource: "dev", value: 1 },
    { task: "ui", resource: "dev", value: 1 },
  ],
};

const response = JSON.parse(schedule(JSON.stringify(data), "{}"));
if (!response.ok) {
  throw new Error(`${response.error.kind}: ${response.error.message} (at ${response.error.path})`);
}
console.log(JSON.stringify(response.result.overallocations, null, 2));
```

Output:

```json
[
  {
    "resource": "dev",
    "windows": [
      {
        "capacity": 1,
        "from": "2026-08-14 08:00",
        "overload": 1,
        "peak": 2,
        "tasks": ["api", "ui"],
        "to": "2026-08-14 17:00"
      },
      {
        "capacity": 1,
        "from": "2026-08-17 08:00",
        "overload": 1,
        "peak": 2,
        "tasks": ["api", "ui"],
        "to": "2026-08-17 17:00"
      }
    ]
  }
]
```

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` is absent when the project has no assignments, and an empty array 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/wasm/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 [`loadProfile`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-load-profile.md>). See [Resource load](<https://docs.dhtmlx.com/planning-engine/wasm/resources-and-assignment/resource-load.md>).
