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, next to the tasks:
- A resource (
ResourceData) is a person, a team or a machine. It has anid, an optionalcapacityand an optionalcalendar_id. - An assignment (
AssignmentData) books one resource on one task at a utilization rate,value.1means 100 %, so0.5is half of someone's time. - A slice is an optional
offset/durationpair 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 values 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), each assignment works on its resource's calendar, so a resource calendar can shift a task. See Resource calendars.
Every schedule of a project with assignments reports result.overallocations: one Overallocation per contended resource, each with a list of windows. Overallocation is data, not an error. Removing it is the job of leveling.
project.solve(data) and schedule(data) validate resources and assignments when you pass the data. These cases throw an InputError with kind: "validation" and the field in path:
- a duplicate resource id (
resources[i].id) - a
capacitybelow0(resources[i].capacity) - a duplicate assignment id (
assignments[i].id) - an assignment
valueof0or less (assignments[i].value) - an assignment that names an unknown task or resource (
assignments[i].task,assignments[i].resource) - an invalid slice:
offsetbelow0, ordurationof0or 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), 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:
const = ;
Output:
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, change capacity with an entry in resources.update of a SchedulingChanges object. capacity: null goes back to the default. Removing a resource also removes all its assignments, and apply 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:
const = new;
const = ;
;
// Move the UI work from Ana to Ben.
const = ;
;
Output:
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 object:
assignments.addtakesAssignmentDatarecords. Without anid, the engine generates one and reports it ineffects.generatedAssignmentsof the apply report.assignments.updatetakes partial records with a requiredid. It can changetask,resource,value,offsetandduration.assignments.removetakes 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:
offsetis where the slice starts, counted from the task start. Absent ornullmeans0.durationis how much work the slice books. Absent ornullmeans 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:
// Whole task, then the last working day only (offset 3 days, 1 day long).
Output:
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: 5on a 4-day task books only the last day. Anoffsetat or past the task's duration books nothing. - Pause time and non-working time don't count toward
offsetorduration. A leveling pause or aleveling_delaymoves 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, the overload metric ofevaluateand the decisions oflevel.
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.
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'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: why leveling left this window. See Leveling resources |
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:
// Both tasks start on Friday 2026-08-14 and run 2 working days.
const = ;
Output:
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:
overallocationsis 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
uiafterapi, the contention is gone, and so is the window. - Detection works in whole buckets, but
fromandtoare 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'
overloadvalues add up to the overloadevaluatereports for that resource at the same basis.
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. See Resource load.