[Canonical page](<https://docs.dhtmlx.com/planning-engine/rust/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::load_profile`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#load_profile>) 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`. It's read-only (`&self`). It never schedules and never changes the project.

The parts fit together like this:

- A [`ProfileQuery`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/resources/profile-query.md>) says what to measure: the range `from`..`to`, which `resources`, the bucket size (`basis`) and whether to list `tasks`. [`ProfileQuery::new(from, to)`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/resources/profile-query.md#new>) gives you every resource, the default basis and no task lists.
- [`LoadProfileOptions`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/resources/load-profile-options.md>) wraps the query and adds an optional `baseline_id`. `From<ProfileQuery>` is implemented, so `project.load_profile(&query.into())` is the short form.
- The result is a [`LoadProfile`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/resources/load-profile.md>): the `basis` it used and one [`ResourceLoad`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/resources/resource-load.md>) per resource. Each `ResourceLoad` has the resource id, its `capacity_mode` and a list of `LoadBucket { 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.0.
- **Same load as leveling.** The profile uses the same sweep as the leveler and the `overallocations` in [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/results/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.

The profile always describes the accepted baseline, so the project has to be solved and clean.

| Failure | Error |
|---|---|
| Project not solved yet, or edited with [`apply`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#apply>) since the last solve | [`Error::Project(ProjectError::NoBaseline)`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/errors/error.md>) |
| `baseline_id` doesn't name the current baseline | `Error::Project(ProjectError::StaleBaseline)` |
| `to <= from`, or more than 100 000 buckets | `Error::Input`, path `to` |
| Unknown id in `resources` | `Error::Input`, path `resources` |

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

## Querying a profile

Solve the project, build a [`ProfileQuery`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/resources/profile-query.md>) and pass it to [`load_profile`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/project/project.md#load_profile>).

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

fn at(day: u32, hour: u32) -> Result<DateTime, Box<dyn std::error::Error>> {
    Ok(DateTime::from_ymd_hm(2026, 8, day, hour, 0).ok_or("invalid date")?)
}

fn task(id: &str, start: DateTime, duration: f64) -> TaskData {
    let mut task = TaskData::new(id);
    task.start_date = Some(start);
    task.duration = Some(duration);
    task
}

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>> {
    // 2026-08-10 is a Monday.
    let data = ProjectData {
        tasks: vec![
            task("a", at(10, 8)?, 2.0),
            task("b", at(10, 8)?, 1.0),
            task("c", at(11, 8)?, 1.0),
            task("d", at(12, 8)?, 1.0),
        ],
        resources: vec![ResourceData {
            id: "dev".into(),
            capacity: None,
            calendar_id: None,
        }],
        assignments: vec![
            assign("a", "dev", 1.0),
            assign("b", "dev", 1.0),
            assign("c", "dev", 0.5),
            assign("d", "dev", 1.0),
        ],
        ..Default::default()
    };

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

    // Sunday 9 up to Friday 14; `to` is exclusive.
    let query = ProfileQuery::new(at(9, 0)?, at(14, 0)?);
    let profile = project.load_profile(&query.into())?;

    println!("basis: {:?}", profile.basis);
    for row in &profile.resources {
        for bucket in &row.buckets {
            println!(
                "{} {}: load {} of {}",
                row.resource, bucket.start, bucket.load, bucket.capacity
            );
        }
    }
    Ok(())
}
```

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

### Narrowing the query

Set `resources` to measure only some resources, and `basis` to change the bucket size. [`ProfileQuery`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/resources/profile-query.md>) is a plain struct, so start from [`ProfileQuery::new`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/resources/profile-query.md#new>) and override fields with struct update syntax.

```rust
use plen_core::{
    AssignmentData, Basis, DateTime, ProfileQuery, Project, ProjectData, ResourceData,
    SolveOptions, TaskData,
};

fn at(day: u32, hour: u32) -> Result<DateTime, Box<dyn std::error::Error>> {
    Ok(DateTime::from_ymd_hm(2026, 8, day, hour, 0).ok_or("invalid date")?)
}

fn task(id: &str, start: DateTime, duration: f64) -> TaskData {
    let mut task = TaskData::new(id);
    task.start_date = Some(start);
    task.duration = Some(duration);
    task
}

fn assign(task: &str, resource: &str) -> AssignmentData {
    AssignmentData {
        id: None,
        task: task.into(),
        resource: resource.into(),
        value: 1.0,
        offset: None,
        duration: None,
    }
}

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

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let data = ProjectData {
        tasks: vec![task("a", at(10, 8)?, 2.0), task("b", at(10, 8)?, 1.0)],
        resources: vec![resource("dev"), resource("qa")],
        assignments: vec![assign("a", "dev"), assign("b", "dev")],
        ..Default::default()
    };

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

    // Monday 07:00 to 10:00, hour buckets, `dev` only.
    let query = ProfileQuery {
        resources: Some(vec!["dev".into()]),
        basis: Some(Basis::Hour),
        ..ProfileQuery::new(at(10, 7)?, at(10, 10)?)
    };
    let profile = project.load_profile(&query.into())?;

    for row in &profile.resources {
        let loads: Vec<f64> = row.buckets.iter().map(|b| b.load).collect();
        println!("{}: {:?}", row.resource, loads);
    }
    Ok(())
}
```

Only `dev` comes back, with three hour buckets: 07:00 is before work starts (0.0), and 08:00 and 09:00 carry both tasks (2.0 each). An id in `resources` that isn't in the project fails with [`Error::Input`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/errors/error.md>) at 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 token from the solve result as `LoadProfileOptions.baseline_id`. If the project was solved again in the meantime, the call fails with [`ProjectError::StaleBaseline`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/errors/project-error.md>) instead of returning load for other dates.

```rust
use plen_core::{
    AssignmentData, DateTime, LoadProfileOptions, ProfileQuery, Project, ProjectData,
    ResourceData, SolveOptions, TaskData,
};

fn at(day: u32, hour: u32) -> Result<DateTime, Box<dyn std::error::Error>> {
    Ok(DateTime::from_ymd_hm(2026, 8, day, hour, 0).ok_or("invalid date")?)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut task = TaskData::new("a");
    task.start_date = Some(at(10, 8)?);
    task.duration = Some(2.0);

    let data = ProjectData {
        tasks: vec![task],
        resources: vec![ResourceData {
            id: "dev".into(),
            capacity: None,
            calendar_id: None,
        }],
        assignments: vec![AssignmentData {
            id: None,
            task: "a".into(),
            resource: "dev".into(),
            value: 1.0,
            offset: None,
            duration: None,
        }],
        ..Default::default()
    };

    let mut project = Project::default();
    let solved = project.solve(Some(data), &SolveOptions::default())?;

    let options = LoadProfileOptions {
        query: ProfileQuery::new(at(10, 0)?, at(15, 0)?),
        baseline_id: solved.baseline_id.clone(),
    };
    let profile = project.load_profile(&options)?;
    println!("{} resource(s) measured", profile.resources.len());
    Ok(())
}
```

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.0). The `capacity_mode` of each [`ResourceLoad`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/resources/resource-load.md>) tells you how it's spread over the buckets:

- [`CapacityMode::Calendar`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/resources/capacity-mode.md>) - 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.
- `CapacityMode::Flat` - every bucket gets the full capacity, weekends and nights included. This applies when the resource has no calendar, when `calendar_mode` is a task mode (the one the dhtmlxGantt adapter uses), 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.

```rust
use plen_core::{
    AssignmentData, Basis, CalendarData, DateTime, ProfileQuery, Project, ProjectData,
    ResourceData, SolveOptions, TaskData, TimeRange, Worktime,
};

fn at(day: u32, hour: u32) -> Result<DateTime, Box<dyn std::error::Error>> {
    Ok(DateTime::from_ymd_hm(2026, 8, day, hour, 0).ok_or("invalid date")?)
}

// Days run Sunday..Saturday.
fn calendar(id: &str, days: [bool; 7], hours: (u16, u16)) -> CalendarData {
    CalendarData {
        id: id.into(),
        worktime: Some(Worktime {
            hours: Some(vec![TimeRange {
                start: hours.0 * 60,
                end: hours.1 * 60,
            }]),
            days: Some(days),
            ..Default::default()
        }),
    }
}

fn resource(id: &str, calendar: Option<&str>) -> ResourceData {
    ResourceData {
        id: id.into(),
        capacity: None,
        calendar_id: calendar.map(Into::into),
    }
}

const MON_FRI: [bool; 7] = [false, true, true, true, true, true, false];
const TUE_SAT: [bool; 7] = [false, false, true, true, true, true, true];

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut task = TaskData::new("a");
    task.start_date = Some(at(11, 8)?); // Tuesday
    task.duration = Some(2.0);

    let data = ProjectData {
        tasks: vec![task],
        calendars: vec![
            calendar("global", MON_FRI, (8, 16)),
            calendar("tue_sat", TUE_SAT, (8, 16)),
            calendar("mornings", MON_FRI, (8, 12)),
        ],
        resources: vec![
            resource("office", None),
            resource("shop", Some("tue_sat")),
            resource("part_time", Some("mornings")),
        ],
        assignments: vec![AssignmentData {
            id: None,
            task: "a".into(),
            resource: "shop".into(),
            value: 1.0,
            offset: None,
            duration: None,
        }],
        ..Default::default()
    };

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

    // One week in day buckets, Sunday 9 to Saturday 15.
    let week = project.load_profile(&ProfileQuery::new(at(9, 0)?, at(16, 0)?).into())?;
    for row in &week.resources {
        let capacity: Vec<f64> = row.buckets.iter().map(|b| b.capacity).collect();
        let load: Vec<f64> = row.buckets.iter().map(|b| b.load).collect();
        println!(
            "{} ({:?}): capacity {:?}, load {:?}",
            row.resource, row.capacity_mode, capacity, load
        );
    }

    // Tuesday 06:00 to 18:00 in hour buckets, for the part-time resource.
    let day = ProfileQuery {
        resources: Some(vec!["part_time".into()]),
        basis: Some(Basis::Hour),
        ..ProfileQuery::new(at(11, 6)?, at(11, 18)?)
    };
    let hours = project.load_profile(&day.into())?;
    let capacity: Vec<f64> = hours.resources[0].buckets.iter().map(|b| b.capacity).collect();
    println!("part_time hourly capacity: {:?}", capacity);
    Ok(())
}
```

What the week shows:

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

## Task lists per bucket

A histogram usually needs to say *who* is overloading a resource. Set `ProfileQuery.tasks` to fill `LoadBucket.tasks`:

| [`ProfileTasks`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/resources/profile-tasks.md>) | `LoadBucket.tasks` |
|---|---|
| `None` (default) | `None` in every bucket |
| `Over` | `Some(ids)` in buckets where `load > capacity`, `Some(vec![])` elsewhere |
| `All` | `Some(ids)` of every task booked in the bucket |

```rust
use plen_core::{
    AssignmentData, DateTime, ProfileQuery, ProfileTasks, Project, ProjectData, ResourceData,
    SolveOptions, TaskData,
};

fn at(day: u32, hour: u32) -> Result<DateTime, Box<dyn std::error::Error>> {
    Ok(DateTime::from_ymd_hm(2026, 8, day, hour, 0).ok_or("invalid date")?)
}

fn task(id: &str, start: DateTime, duration: f64) -> TaskData {
    let mut task = TaskData::new(id);
    task.start_date = Some(start);
    task.duration = Some(duration);
    task
}

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

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let data = ProjectData {
        tasks: vec![
            task("a", at(10, 8)?, 2.0),
            task("b", at(10, 8)?, 1.0),
            task("c", at(11, 8)?, 1.0),
            task("d", at(12, 8)?, 1.0),
        ],
        resources: vec![ResourceData {
            id: "dev".into(),
            capacity: None,
            calendar_id: None,
        }],
        assignments: vec![
            assign("a", 1.0),
            assign("b", 1.0),
            assign("c", 0.5),
            assign("d", 1.0),
        ],
        ..Default::default()
    };

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

    // Monday 10 to Thursday 13.
    let query = ProfileQuery {
        tasks: ProfileTasks::Over,
        ..ProfileQuery::new(at(10, 0)?, at(14, 0)?)
    };
    let profile = project.load_profile(&query.into())?;

    for bucket in &profile.resources[0].buckets {
        println!("{}: load {}, tasks {:?}", bucket.start, bucket.load, bucket.tasks);
    }
    Ok(())
}
```

With `Over`, Monday lists `a` and `b`, and Tuesday lists `a` and `c`. Wednesday and Thursday aren't overloaded, so they get empty lists. Switch to `ProfileTasks::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/rust/api/core/results/schedule-result.md>) of a project with assignments carries `overallocations`: per-resource windows with `peak`, `overload` and the contending `tasks`. See [Leveling resources](<https://docs.dhtmlx.com/planning-engine/rust/resources-and-assignment/leveling-resources.md>) for how to remove the overload.

## Load of hypothetical dates

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

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

fn at(day: u32, hour: u32) -> Result<DateTime, Box<dyn std::error::Error>> {
    Ok(DateTime::from_ymd_hm(2026, 8, day, hour, 0).ok_or("invalid date")?)
}

fn task(id: &str, start: DateTime, duration: f64) -> TaskData {
    let mut task = TaskData::new(id);
    task.start_date = Some(start);
    task.duration = Some(duration);
    task
}

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 main() -> Result<(), Box<dyn std::error::Error>> {
    let data = ProjectData {
        tasks: vec![
            task("a", at(10, 8)?, 2.0),
            task("b", at(10, 8)?, 1.0),
            task("c", at(11, 8)?, 1.0),
        ],
        resources: vec![resource("dev"), resource("qa")],
        assignments: vec![
            assign("a", "dev", 1.0),
            assign("b", "dev", 1.0),
            assign("c", "dev", 0.5),
        ],
        ..Default::default()
    };

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

    // What if `b` moves from `dev` to `qa`?
    let mut changes = SchedulingChanges::default();
    let mut update = AssignmentUpdate::new("b:dev");
    update.resource = Patch::Value("qa".into());
    changes.assignments.update.push(update);

    let options = PreviewOptions {
        profile: Some(ProfileQuery::new(at(10, 0)?, at(12, 0)?)),
        ..Default::default()
    };
    let preview = project.preview(&changes, &options)?;

    if let Some(profile) = &preview.profile {
        for row in &profile.resources {
            let loads: Vec<f64> = row.buckets.iter().map(|b| b.load).collect();
            println!("previewed {}: {:?}", row.resource, loads);
        }
    }

    // The baseline still has both tasks on `dev`.
    let base = project.load_profile(&ProfileQuery::new(at(10, 0)?, at(12, 0)?).into())?;
    let loads: Vec<f64> = base.resources[0].buckets.iter().map(|b| b.load).collect();
    println!("baseline dev: {:?}", loads);
    Ok(())
}
```

In the preview, `dev` drops to 1.0 on Monday and keeps 1.5 on Tuesday, and `qa` gets 1.0 on Monday. The baseline profile still shows 2.0 and 1.5 for `dev`. Without `PreviewOptions.profile`, `preview.profile` is `None`.

`PreviewOptions.profile` takes a bare `ProfileQuery`, not [`LoadProfileOptions`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/resources/load-profile-options.md>), so it has no `baseline_id` of its own. To guard a preview against a stale baseline, set `PreviewOptions.baseline_id`. An invalid query fails the whole preview with [`Error::Input`](<https://docs.dhtmlx.com/planning-engine/rust/api/core/errors/error.md>).

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