[Canonical page](<https://docs.dhtmlx.com/planning-engine/rust/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 every scheduled plan reports.

## How resources work

Three records describe staffing, and they all live in [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/data/project-data.md>) next to the tasks:

- A **resource** ([`ResourceData`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/data/resource-data.md>)) 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/rust/api/core/data/assignment-data.md>)) books one resource on one task at a utilization rate, `value`. `1.0` 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: Some(2.0)` 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.0` 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.0` 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. The one exception is calendars: in the default [`CalendarMode::Resource`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/data/calendar-mode.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/rust/scheduling/working-time-calendars.md#resource-calendars>).

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

[`solve(Some(data))`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#solve>) and [`schedule`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/schedule.md>) validate resources and assignments when you pass the data. These cases fail with [`Error::Input`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/errors/error.md>) at the field path:

- a duplicate resource id (`resources[i].id`)
- a `capacity` that isn't finite or is below `0`
- a duplicate assignment id (`assignments[i].id`)
- an assignment `value` that isn't finite or is `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 not finite, `duration` of `0` or less or not finite (`assignments[i].offset`, `assignments[i].duration`)

In [`Validation::Repair`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/validation.md>) mode the engine clears an invalid slice and adds a [`Note::Repaired`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/results/note.md>) instead, 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:

```rust
use plen_core::{
    schedule, AssignmentData, DateTime, ProjectData, ResourceData, SolveOptions, TaskData,
};

fn assign(task: &str, resource: &str, value: f64) -> AssignmentData {
    AssignmentData {
        id: Some(format!("{task}:{resource}").into()),
        task: task.into(),
        resource: resource.into(),
        value,
        offset: None,
        duration: None,
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Two 2-day tasks, both starting Monday 2026-08-10.
    let mut tasks = Vec::new();
    for id in ["api", "ui"] {
        let mut task = TaskData::new(id);
        task.start_date = DateTime::from_ymd_hm(2026, 8, 10, 8, 0);
        task.duration = Some(2.0);
        tasks.push(task);
    }

    let data = ProjectData {
        tasks,
        resources: vec![
            // One developer: capacity falls back to
            // config.default_resource_capacity (1.0).
            ResourceData { id: "dev".into(), capacity: None, calendar_id: None },
            // A QA team of two people.
            ResourceData { id: "qa".into(), capacity: Some(2.0), calendar_id: None },
        ],
        assignments: vec![
            assign("api", "dev", 1.0),
            assign("ui", "dev", 1.0),
            assign("api", "qa", 1.0),
            assign("ui", "qa", 1.0),
        ],
        ..Default::default()
    };

    let result = schedule(data, &SolveOptions::default())?;
    for resource in result.overallocations.iter().flatten() {
        for window in &resource.windows {
            println!(
                "{}: {} -> {}, peak {} of {}",
                resource.resource, window.from, window.to, window.peak, window.capacity
            );
        }
    }
    Ok(())
}
```

Output:

```text
dev: 2026-08-10 08:00 -> 2026-08-11 17:00, peak 2 of 1
```

Both resources carry a load of `2.0` on Monday and Tuesday. Only `dev` is overallocated, because its capacity is the default `1.0`. `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`. With `8.0`, for example, a `value` of `4.0` is half of a resource. A resource with its own `capacity` ignores the default.

In a live [`Project`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md>), change capacity with a [`ResourceUpdate`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/changes/resource-update.md>) in `SchedulingChanges.resources.update`. `capacity: Patch::Null` goes back to the default. Removing a resource also removes all its assignments. They're listed in `ApplyReport.effects.removed_assignments`.

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

```rust
use plen_core::{
    AssignmentData, AssignmentUpdate, DateTime, Patch, Project, ProjectData, ResourceData,
    ScheduleResult, SchedulingChanges, SolveOptions, TaskData,
};

fn assign(task: &str, resource: &str, value: f64) -> AssignmentData {
    AssignmentData {
        id: Some(format!("{task}:{resource}").into()),
        task: task.into(),
        resource: resource.into(),
        value,
        offset: None,
        duration: None,
    }
}

fn resource(id: &str) -> ResourceData {
    ResourceData { id: id.into(), capacity: None, calendar_id: None }
}

fn report(label: &str, result: &ScheduleResult) {
    let busy: Vec<String> = result
        .overallocations
        .iter()
        .flatten()
        .map(|r| r.resource.to_string())
        .collect();
    println!("{label}: overallocated {busy:?}");
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut tasks = Vec::new();
    for id in ["api", "ui"] {
        let mut task = TaskData::new(id);
        task.start_date = DateTime::from_ymd_hm(2026, 8, 10, 8, 0);
        task.duration = Some(2.0);
        tasks.push(task);
    }

    let data = ProjectData {
        tasks,
        resources: vec![resource("ana"), resource("ben"), resource("lead")],
        assignments: vec![
            assign("api", "ana", 1.0),
            assign("ui", "ana", 1.0),
            // The lead spends half of their time on each task.
            assign("api", "lead", 0.5),
            assign("ui", "lead", 0.5),
        ],
        ..Default::default()
    };

    let mut project = Project::default();
    let result = project.solve(Some(data), &SolveOptions::default())?;
    report("initial", &result);

    // Move the UI work from Ana to Ben.
    let mut update = AssignmentUpdate::new("ui:ana");
    update.resource = Patch::Value("ben".into());
    let mut changes = SchedulingChanges::default();
    changes.assignments.update.push(update);
    let applied = project.apply(&changes)?;
    if let Some(rejected) = applied.rejected.first() {
        return Err(format!("{}: {}", rejected.path, rejected.message).into());
    }

    let result = project.solve(None, &SolveOptions::default())?;
    report("after reassign", &result);
    Ok(())
}
```

Output:

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

The lead's two half-time assignments add up to `1.0`, so only Ana shows up at first. The [`AssignmentUpdate`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/changes/assignment-update.md>) 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/rust/api/core/changes/scheduling-changes.md>):

- `assignments.add` takes an [`AssignmentData`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/data/assignment-data.md>). With `id: None` the engine generates an id and reports it in `ApplyReport.effects.generated_assignments`.
- `assignments.update` 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 the result carries a [`Note::DroppedAssignment`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/results/note.md>). 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. `None` means `0`.
- `duration`: how much work the slice books. `None` 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:

```rust
use plen_core::{
    schedule, AssignmentData, DateTime, ProjectData, ResourceData, SolveOptions, TaskData,
};

fn resource(id: &str) -> ResourceData {
    ResourceData { id: id.into(), capacity: None, calendar_id: None }
}

fn project(review_slice: Option<(f64, f64)>) -> ProjectData {
    // `build`: 4 working days, Monday to Thursday.
    let mut build = TaskData::new("build");
    build.start_date = DateTime::from_ymd_hm(2026, 8, 10, 8, 0);
    build.duration = Some(4.0);
    // `plan`: Monday only.
    let mut plan = TaskData::new("plan");
    plan.start_date = DateTime::from_ymd_hm(2026, 8, 10, 8, 0);
    plan.duration = Some(1.0);

    ProjectData {
        tasks: vec![build, plan],
        resources: vec![resource("dev"), resource("lead")],
        assignments: vec![
            AssignmentData {
                id: Some("build:dev".into()),
                task: "build".into(),
                resource: "dev".into(),
                value: 1.0,
                offset: None,
                duration: None,
            },
            // The lead reviews `build`: the whole task, or only a slice of it.
            AssignmentData {
                id: Some("build:lead".into()),
                task: "build".into(),
                resource: "lead".into(),
                value: 1.0,
                offset: review_slice.map(|(offset, _)| offset),
                duration: review_slice.map(|(_, duration)| duration),
            },
            AssignmentData {
                id: Some("plan:lead".into()),
                task: "plan".into(),
                resource: "lead".into(),
                value: 1.0,
                offset: None,
                duration: None,
            },
        ],
        ..Default::default()
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let options = SolveOptions { include_unchanged: true, ..Default::default() };
    // Whole task, then the last working day only (offset 3 days, 1 day long).
    for slice in [None, Some((3.0, 1.0))] {
        let result = schedule(project(slice), &options)?;
        println!("slice {slice:?}");
        for update in &result.updates {
            if let (Some(start), Some(end)) = (update.start_date, update.end_date) {
                println!("  {}: {start} -> {end}", update.id);
            }
        }
        for resource in result.overallocations.iter().flatten() {
            for window in &resource.windows {
                println!("  {} overallocated: {} -> {}", resource.resource, window.from, window.to);
            }
        }
    }
    Ok(())
}
```

Output:

```text
slice None
  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 Some((3.0, 1.0))
  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: Some(3.0), duration: Some(5.0)` 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 [`CalendarMode::Resource`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/data/calendar-mode.md>) 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.
- Load consumers all see the sliced load: `overallocations`, `load_profile`, the overload metric of `evaluate` and the decisions of `level`.

In a live project, edit a slice with `AssignmentUpdate.offset` and `AssignmentUpdate.duration`. [`Patch::Null`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/changes/patch.md>) 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 is a rejected operation in `ApplyReport.rejected`.

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

Planning Engine has no assignment mode like dhtmlxGantt's `mode` field. Every resource is checked for overallocation, and a stored `mode` is ignored. Convert gantt's modes to slices when you import. See [Import from dhtmlxGantt](<https://docs.dhtmlx.com/planning-engine/rust/how-to/import-from-dhtmlxgantt.md>).

## Overallocation windows

A window is a run of contiguous overallocated buckets of one resource. Each [`OverallocationWindow`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/resources/overallocation-window.md>) 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/rust/api/core/project/project.md#level>): why leveling left this window. See [Leveling resources](<https://docs.dhtmlx.com/planning-engine/rust/resources-and-assignment/leveling-resources.md>) |

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

```rust
use plen_core::{
    schedule, AssignmentData, DateTime, ProjectData, ResourceData, SolveOptions, TaskData,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Both tasks start on Friday 2026-08-14 and run 2 working days.
    let mut tasks = Vec::new();
    let mut assignments = Vec::new();
    for id in ["api", "ui"] {
        let mut task = TaskData::new(id);
        task.start_date = DateTime::from_ymd_hm(2026, 8, 14, 8, 0);
        task.duration = Some(2.0);
        tasks.push(task);
        assignments.push(AssignmentData {
            id: None,
            task: id.into(),
            resource: "dev".into(),
            value: 1.0,
            offset: None,
            duration: None,
        });
    }

    let data = ProjectData {
        tasks,
        resources: vec![ResourceData { id: "dev".into(), capacity: None, calendar_id: None }],
        assignments,
        ..Default::default()
    };

    let result = schedule(data, &SolveOptions::default())?;
    for resource in result.overallocations.iter().flatten() {
        for w in &resource.windows {
            let tasks: Vec<String> = w.tasks.iter().map(|id| id.to_string()).collect();
            println!(
                "{}: {} -> {} peak={} capacity={} overload={} tasks={tasks:?}",
                resource.resource, w.from, w.to, w.peak, w.capacity, w.overload
            );
        }
    }
    Ok(())
}
```

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 else windows tell you, and what they don't:

- `overallocations` is `None` when the project has no assignments, and an empty list 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/rust/api/core/project/project.md#evaluate>) reports for that resource at the same basis.

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