Change who works when, the plan reshapes
Four people, ten tasks, one calendar each. The seven buttons under the chart are a person’s working week. Switch a day off and every task of that person stretches over the days they still work, and everything after it moves. Tick Day off to take one date out of their calendar.
Ben works Monday to Thursday and Dee works Tuesday to Saturday, so their bars already pause on days the others work. The engine places each task on its assignee’s calendar. The page does no date math: it sends the calendars and draws the dates and bookings that come back.
The task works on its person’s calendar
A task without its own calendar_id works on the calendar of its assigned resource. A task with both works on their intersection, and a task with several people spans all their calendars. This demo uses the simplest case: one person per task, no task calendars.
// One calendar per person. Tasks have none of their own,
// so each task works on the calendar of the person assigned to it.
project.solve({
config: { gap_behavior: "compress", project_start: "2026-08-10 00:00" },
calendars: [
{ id: "global", worktime: { hours: ["8:00-12:00", "13:00-17:00"], days: [0, 1, 1, 1, 1, 1, 0] } }, // the milestone has no person
{ id: "ben", worktime: { hours: ["8:00-12:00", "13:00-17:00"], days: [0, 1, 1, 1, 1, 0, 0] } }, // Mon–Thu
{ id: "dee", worktime: { hours: ["8:00-12:00", "13:00-17:00"], days: [0, 0, 1, 1, 1, 1, 1] } }, // Tue–Sat
// …
],
resources: [{ id: "ben", calendar_id: "ben" }, { id: "dee", calendar_id: "dee" } /* … */],
assignments: [{ task: 2, resource: "ben", value: 1 } /* … */],
tasks: [{ id: 2, start_date: "2026-08-10 00:00", duration: 3 } /* … */],
links: [/* … */],
}, { includeUnchanged: true });A day off is one date
The day-off field is prefilled, so you only tick the box. The date is added to the person’s calendar as a non-working exception. If none of their tasks falls on that day nothing moves, and the log says so.
// A day off is a date exception in that person's calendar.
{ id: "ana", worktime: {
hours: ["8:00-12:00", "13:00-17:00"],
days: [0, 1, 1, 1, 1, 1, 0],
dates: { "2026-08-11": false },
} }Every change is a full solve
The change language edits tasks, links, resources and assignments, but not calendars. So each click sends the whole plan again. Every task starts from the project start, andgap_behavior: "compress" pulls it to the earliest time its links and calendar allow. No start date from the previous plan is kept, so giving someone a day back pulls the work earlier again. With ten tasks the solve and the load read together take about a millisecond.
Bars and load come from the engine
The bars use the start and end the engine returned. To show the days a task does not use, the demo asks loadProfile which tasks each person works on per day, and draws the other days as a pause. The strip under each person reads the same buckets: a booked day, a free day, or a day with capacity 0 because they do not work.
// Where the work actually sits: one bucket per day, per person.
const profile = project.loadProfile({ from: "2026-08-10 00:00", to: "2026-10-05 00:00", basis: "day", tasks: "all" });
// A bar covers start → end; the days whose bucket lists the task are drawn as work,
// the rest as a pause.
const work = buckets[day].tasks.includes(task.id);The team load demo uses the same call for a whole team, with part-time calendars, leave and shared capacity.