[Canonical page](<https://docs.dhtmlx.com/planning-engine/js/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 all live in [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/js/api/types/project-data.md>), next to the tasks:

- A **resource** ([`ResourceData`](<https://docs.dhtmlx.com/planning-engine/js/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** ([`AssignmentData`](<https://docs.dhtmlx.com/planning-engine/js/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.
- 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 gets `config.default_resource_capacity`, 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 a `"day"` 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` 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 `calendar_mode: "resource"` (see [`SchedulingConfig`](<https://docs.dhtmlx.com/planning-engine/js/api/types/scheduling-config.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/js/scheduling/working-time-calendars.md#resource-calendars>).

Every schedule of a project with assignments reports `result.overallocations`: one [`Overallocation`](<https://docs.dhtmlx.com/planning-engine/js/api/types/overallocation.md>) per contended resource, each with a list of windows. Overallocation is data, not an error. Removing it is the job of [leveling](<https://docs.dhtmlx.com/planning-engine/js/resources-and-assignment/leveling-resources.md>).

[`project.solve(data)`](<https://docs.dhtmlx.com/planning-engine/js/api/project-solve.md>) and [`schedule(data)`](<https://docs.dhtmlx.com/planning-engine/js/api/schedule.md>) validate resources and assignments when you pass the data. These cases throw an [`InputError`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md#inputerror>) with `kind: "validation"` and the field in `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`, or `duration` of `0` or less (`assignments[i].offset`, `assignments[i].duration`)

Non-finite numbers (`NaN`, `Infinity`) never reach these checks: any non-finite number in the input throws an `InputError` with the message "Numbers must be finite".

With `new Project({ validation: "repair" })` (see [`Project`](<https://docs.dhtmlx.com/planning-engine/js/api/project.md>)), the engine clears an invalid slice instead and adds a `repaired` note to `result.notes`, 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:

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

const result = schedule({
  // 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 },
  ],
});

for (const { resource, windows = [] } of result.overallocations ?? []) {
  for (const window of windows) {
    console.log(`${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`. `qa` sits exactly at its capacity, and that isn't overallocation.

To change the default for every resource without a `capacity`, set `config.default_resource_capacity`. 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/js/api/project.md>), change capacity with an entry in `resources.update` of a [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/js/api/types/scheduling-changes.md>) object. `capacity: null` goes back to the default. Removing a resource also removes all its assignments, and [`apply`](<https://docs.dhtmlx.com/planning-engine/js/api/project-apply.md>) lists them in `effects.removedAssignments`.

## 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
import { Project } from "plen-js";

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

const project = new Project();
const result = project.solve({
  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 },
  ],
});
report("initial", result);

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

report("after reassign", project.solve(null));
```

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 update 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/js/api/types/scheduling-changes.md>) object:

- `assignments.add` takes [`AssignmentData`](<https://docs.dhtmlx.com/planning-engine/js/api/types/assignment-data.md>) records. Without an `id`, the engine generates one and reports it in `effects.generatedAssignments` of the apply report.
- `assignments.update` takes partial records with a required `id`. It 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 `result.notes` carries a `dropped-assignment` note for it. 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 `duration_unit`, measured on the assignment's calendar:

- `offset` is where the slice starts, counted from the task start. Absent or `null` means `0`.
- `duration` is 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
import { schedule } from "plen-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 result = schedule(project(slice), { includeUnchanged: true });
  console.log(`slice ${JSON.stringify(slice)}`);
  for (const update of result.updates) {
    console.log(`  ${update.id}: ${update.start_date} -> ${update.end_date}`);
  }
  for (const { resource, windows = [] } of result.overallocations ?? []) {
    for (const window of windows) {
      console.log(`  ${resource} overallocated: ${window.from} -> ${window.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 both `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. An `offset` at or past the task's duration books nothing.
- Pause time and non-working time don't count toward `offset` or `duration`. A leveling 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 `calendar_mode: "resource"` 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/js/api/project-load-profile.md>), the overload metric of [`evaluate`](<https://docs.dhtmlx.com/planning-engine/js/api/project-evaluate.md>) and the decisions of [`level`](<https://docs.dhtmlx.com/planning-engine/js/api/project-level.md>).

In a live project, 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. An invalid value doesn't throw. It becomes a rejected operation in the apply report's `rejected` list.

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

Assignments have no mode. Every resource is checked for overallocation, and a stored `mode` is ignored as an unknown field. Use slices to shape how an assignment's work is spread.

## Overallocation windows

A window is a run of contiguous overallocated buckets of one resource. Each entry in an [`Overallocation`](<https://docs.dhtmlx.com/planning-engine/js/api/types/overallocation.md>)'s `windows` 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/js/api/project-level.md>): why leveling left this window. See [Leveling resources](<https://docs.dhtmlx.com/planning-engine/js/resources-and-assignment/leveling-resources.md>) |

`from` and `to` are `"YYYY-MM-DD HH:MM"` strings, or local `Date` objects with `new Project({ dates: "date" })`.

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

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

// Both tasks start on Friday 2026-08-14 and run 2 working days.
const result = schedule({
  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 },
  ],
});

for (const { resource, windows = [] } of result.overallocations ?? []) {
  for (const w of windows) {
    console.log(
      `${resource}: ${w.from} -> ${w.to} peak=${w.peak} capacity=${w.capacity} ` +
        `overload=${w.overload} tasks=${JSON.stringify(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 windows tell you, and what they don't:

- `overallocations` is absent (`undefined`) 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/js/api/project-evaluate.md>) reports for that resource at the same basis.

:::note
`windows` is absent, not an empty array, when a resource has no overallocated window.
:::

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