[Canonical page](<https://docs.dhtmlx.com/planning-engine/java/scheduling/working-time-calendars/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Working time and calendars

This guide shows how to tell Planning Engine when work can happen: working hours, weekdays, holidays and short days. It also covers how tasks and resources pick a calendar, and how to turn calendars off.

## How calendars work

A task's `duration` is **working time**. A 16-hour task doesn't end 16 hours after it starts. It ends once it has used 16 hours of its calendar's working time, so nights, weekends and holidays stretch it. Lags work the same way. `levelingDelay` and `pauses` are the exception: they're elapsed wall-clock time.

A calendar is a [`CalendarData`](<https://docs.dhtmlx.com/planning-engine/java/api/data/calendar-data.md>): an `id` plus an optional [`Worktime`](<https://docs.dhtmlx.com/planning-engine/java/api/data/worktime.md>) with three parts:

- [`hours`](<https://docs.dhtmlx.com/planning-engine/java/api/data/worktime.md#hours>): the working ranges of a day, as [`TimeRange`](<https://docs.dhtmlx.com/planning-engine/java/api/data/worktime.md#timerange>) values in minutes since midnight. Default: 08:00-12:00 and 13:00-17:00.
- [`days`](<https://docs.dhtmlx.com/planning-engine/java/api/data/worktime.md#days>): a `boolean[]` of exactly seven flags, **index 0 is Sunday**. Default: Monday to Friday.
- [`dates`](<https://docs.dhtmlx.com/planning-engine/java/api/data/worktime.md#dates>): exceptions in a [`SortedDict<Moment, DayRule>`](<https://docs.dhtmlx.com/planning-engine/java/api/support/sorted-dict.md>), keyed by the date's midnight [`Moment`](<https://docs.dhtmlx.com/planning-engine/java/api/data/moment.md>). A [`DayRule`](<https://docs.dhtmlx.com/planning-engine/java/api/data/worktime.md#dayrule>) makes a date `DayRule.Off`, `DayRule.On` (working with the calendar's `hours`) or working with its own `DayRule.Hours` ranges.

Every project has a calendar named `"global"`. If you don't define one, the engine uses the built-in Monday-Friday, 08:00-12:00 and 13:00-17:00. A calendar in [`ProjectData.calendars`](<https://docs.dhtmlx.com/planning-engine/java/api/data/project-data.md#fields>) with the id `"global"` replaces it.

Which calendar a piece of work uses depends on [`SchedulingConfig.calendarMode`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md#calendarmode>) ([`CalendarMode`](<https://docs.dhtmlx.com/planning-engine/java/api/data/calendar-mode.md>)):

| Mode | A task works on |
|---|---|
| `RESOURCE` (default) | each assignment's calendar: the resource's calendar, intersected with the task's own calendar when both are set |
| `TASK` | the task's `calendarId`, else `"global"`; resource calendars are ignored |
| `TASK_INHERITED` | the task's `calendarId`, else the nearest ancestor's, else `"global"` |

`RESOURCE` follows MS Project. `TASK` and `TASK_INHERITED` use task calendars only.

[`SchedulingConfig.useCalendars`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md#usecalendars>) overrides all of this. It's `true` by default. Set it to `false` and every calendar becomes flat 24/7 time.

The engine validates calendars when you pass the data to [`Project#solve(data, …)`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) or [`Engine.schedule`](<https://docs.dhtmlx.com/planning-engine/java/api/engine-schedule.md>). These cases throw an unchecked [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>) with the field path in [`path`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md#path>):

- a duplicate calendar id
- a calendar with no working time
- an hour range outside 00:00-24:00
- `days` with a length other than 7
- a task or resource `calendarId` that names no calendar (for resources, only in `RESOURCE` mode)
- in `RESOURCE` mode, a task calendar and a resource calendar that share no weekly working time (reported at `assignments[i]`)

Calendars are part of the snapshot only. [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/scheduling-changes.md>) can't add or edit a calendar. To change one, send the full data again with `solve(data, …)`. Edits can still point tasks and resources at existing calendars through `calendarId`.

## Defining calendars

Replace `"global"` to change the working week for the whole project. The calendar below works Monday to Friday, 09:00-17:00. Thursday the 13th is a short day and Friday the 14th is a holiday:

```java
import org.dhtmlx.plen.*;
import java.util.List;

public class DefineCalendar {
    static Moment at(int day, int hour) {
        Moment m = Moment.fromYmdHm(2026, 8, day, hour, 0);
        if (m == null) {
            throw new IllegalArgumentException("invalid date");
        }
        return m;
    }

    public static void main(String[] args) {
        SortedDict<Moment, DayRule> dates = new SortedDict<>();
        // Thursday: a short day, 09:00-13:00.
        dates.put(at(13, 0), new DayRule.Hours(List.of(new TimeRange(9 * 60, 13 * 60))));
        // Friday: a public holiday.
        dates.put(at(14, 0), new DayRule.Off());

        CalendarData global = new CalendarData()
            .withId(Id.str("global"))
            .withWorktime(new Worktime()
                .withHours(List.of(new TimeRange(9 * 60, 17 * 60)))
                .withDays(new boolean[] {false, true, true, true, true, true, false}) // Sun..Sat
                .withDates(dates));

        ProjectData data = new ProjectData()
            .withConfig(new SchedulingConfig().withDurationUnit(TimeUnit.HOUR))
            .withCalendars(List.of(global))
            .withTasks(List.of(
                new TaskData(Id.str("build")).withStartDate(at(10, 9)).withDuration(36.0))); // Monday

        ScheduleResult result = Engine.schedule(data, new SolveOptions().withIncludeUnchanged(true));
        for (Mutation update : result.updates) {
            System.out.println(update.id + ": " + update.startDate + " -> " + update.endDate);
        }
    }
}
```

Output:

```text
build: 2026-08-10 09:00 -> 2026-08-17 17:00
```

The 36 hours are Monday to Wednesday (24 h), the short Thursday (4 h) and the next Monday (8 h). The holiday and the weekend hold no work.

A `Worktime` field you leave as `null` keeps its default. [`new Worktime().withDays(...)`](<https://docs.dhtmlx.com/planning-engine/java/api/data/worktime.md#withdays>) changes the weekdays and keeps the 08:00-12:00, 13:00-17:00 hours. `dates` starts as an empty map, so you can also fill it in place with `worktime.dates.put(...)`.

### Date exception keys

[`Worktime.dates`](<https://docs.dhtmlx.com/planning-engine/java/api/data/worktime.md#dates>) is keyed by a full [`Moment`](<https://docs.dhtmlx.com/planning-engine/java/api/data/moment.md>), but only the date part counts. Use the date's midnight as the key, as the example does with `at(13, 0)`.

Two keys on the same day collide. Put a rule at 00:00 and another at 12:00 on the same date, and the later one replaces the earlier rule without an error. Build every key at midnight and this can't happen.

## Task calendars

Set [`TaskData.calendarId`](<https://docs.dhtmlx.com/planning-engine/java/api/data/task-data.md#fields>) to run one task on a different calendar. Here the `migrate` task runs on a night shift, while `review` stays on the default `"global"` calendar:

```java
import org.dhtmlx.plen.*;
import java.util.List;

public class TaskCalendars {
    static Moment at(int day, int hour) {
        Moment m = Moment.fromYmdHm(2026, 8, day, hour, 0);
        if (m == null) {
            throw new IllegalArgumentException("invalid date");
        }
        return m;
    }

    public static void main(String[] args) {
        // 22:00-06:00: the range ends before it starts, so it crosses midnight
        // and belongs to the day it starts on.
        CalendarData night = new CalendarData()
            .withId(Id.str("night"))
            .withWorktime(new Worktime().withHours(List.of(new TimeRange(22 * 60, 6 * 60))));

        ProjectData data = new ProjectData()
            .withConfig(new SchedulingConfig().withDurationUnit(TimeUnit.HOUR))
            .withCalendars(List.of(night))
            .withTasks(List.of(
                new TaskData(Id.str("review")).withStartDate(at(10, 8)).withDuration(16.0),
                new TaskData(Id.str("migrate"))
                    .withStartDate(at(10, 22))
                    .withDuration(16.0)
                    .withCalendarId(Id.str("night"))));

        ScheduleResult result = Engine.schedule(data, new SolveOptions().withIncludeUnchanged(true));
        for (Mutation update : result.updates) {
            System.out.println(update.id + ": " + update.startDate + " -> " + update.endDate);
        }
    }
}
```

Output:

```text
review: 2026-08-10 08:00 -> 2026-08-11 17:00
migrate: 2026-08-10 22:00 -> 2026-08-12 06:00
```

The night shift's days are the default Monday to Friday. Monday's shift runs until Tuesday 06:00, because a range that crosses midnight belongs to the day it starts on.

A task's own calendar (else `"global"`) also matters in `RESOURCE` mode. Milestones, lag without a link calendar and slack always use it.

### Inheriting a calendar from a summary

In `TASK` and `RESOURCE` modes a child doesn't look at its parent's calendar. Switch to [`CalendarMode.TASK_INHERITED`](<https://docs.dhtmlx.com/planning-engine/java/api/data/calendar-mode.md>) to make a task without a `calendarId` use the nearest ancestor's:

```java
import org.dhtmlx.plen.*;
import java.util.List;

public class InheritedCalendar {
    public static void main(String[] args) {
        Moment friday = Moment.fromYmdHm(2026, 8, 14, 8, 0);
        if (friday == null) {
            throw new IllegalArgumentException("invalid date");
        }

        CalendarData sixDayWeek = new CalendarData()
            .withId(Id.str("six-day"))
            .withWorktime(new Worktime()
                .withDays(new boolean[] {false, true, true, true, true, true, true}));

        ProjectData data = new ProjectData()
            .withConfig(new SchedulingConfig()
                .withDurationUnit(TimeUnit.HOUR)
                .withCalendarMode(CalendarMode.TASK_INHERITED))
            .withCalendars(List.of(sixDayWeek))
            .withTasks(List.of(
                new TaskData(Id.str("phase"))
                    .withTaskType(TaskType.SUMMARY)
                    .withCalendarId(Id.str("six-day")),
                new TaskData(Id.str("pour"))
                    .withParent(Id.str("phase"))
                    .withStartDate(friday)
                    .withDuration(16.0)));

        ScheduleResult result = Engine.schedule(data, new SolveOptions().withIncludeUnchanged(true));
        for (Mutation update : result.updates) {
            System.out.println(update.id + ": " + update.startDate + " -> " + update.endDate);
        }
    }
}
```

Output:

```text
pour: 2026-08-14 08:00 -> 2026-08-15 17:00
phase: 2026-08-14 08:00 -> 2026-08-15 17:00
```

`pour` works Friday and Saturday on the summary's six-day week. In the default mode it would wait for Monday.

## Resource calendars

In the default `RESOURCE` mode each assignment works on its resource's calendar, and the task's dates follow its assignments. Give a resource a calendar through [`ResourceData.calendarId`](<https://docs.dhtmlx.com/planning-engine/java/api/data/resource-data.md#fields>). The example runs the same project in `RESOURCE` and `TASK` mode:

```java
import org.dhtmlx.plen.*;
import java.util.List;

public class ResourceCalendars {
    static ProjectData project(CalendarMode mode) {
        Moment friday = Moment.fromYmdHm(2026, 8, 14, 8, 0);
        if (friday == null) {
            throw new IllegalArgumentException("invalid date");
        }

        CalendarData tueSat = new CalendarData()
            .withId(Id.str("tue-sat"))
            .withWorktime(new Worktime()
                .withDays(new boolean[] {false, false, true, true, true, true, true}));

        return new ProjectData()
            .withConfig(new SchedulingConfig()
                .withDurationUnit(TimeUnit.HOUR)
                .withCalendarMode(mode))
            .withCalendars(List.of(tueSat))
            .withTasks(List.of(
                // `prepare` has no resources, so it runs on the global calendar
                // and ends Friday 17:00.
                new TaskData(Id.str("prepare")).withStartDate(friday).withDuration(8.0),
                new TaskData(Id.str("inspect")).withStartDate(friday).withDuration(16.0)))
            .withLinks(List.of(
                new LinkData()
                    .withId(Id.str("l1"))
                    .withSource(Id.str("prepare"))
                    .withTarget(Id.str("inspect"))))
            .withResources(List.of(
                new ResourceData().withId(Id.str("dana")).withCalendarId(Id.str("tue-sat"))))
            .withAssignments(List.of(
                new AssignmentData()
                    .withId(Id.str("inspect:dana"))
                    .withTask(Id.str("inspect"))
                    .withResource(Id.str("dana"))
                    .withValue(1.0)));
    }

    public static void main(String[] args) {
        SolveOptions options = new SolveOptions().withIncludeUnchanged(true);
        for (CalendarMode mode : List.of(CalendarMode.RESOURCE, CalendarMode.TASK)) {
            ScheduleResult result = Engine.schedule(project(mode), options);
            for (Mutation update : result.updates) {
                System.out.println(mode + " " + update.id + ": " + update.startDate + " -> " + update.endDate);
            }
        }
    }
}
```

Output:

```text
RESOURCE prepare: 2026-08-14 08:00 -> 2026-08-14 17:00
RESOURCE inspect: 2026-08-15 08:00 -> 2026-08-18 17:00
TASK prepare: 2026-08-14 08:00 -> 2026-08-14 17:00
TASK inspect: 2026-08-17 08:00 -> 2026-08-18 17:00
```

In `RESOURCE` mode Dana works Saturday, so `inspect` starts right after `prepare` and runs Saturday and Tuesday. In `TASK` mode her calendar is ignored and `inspect` waits for Monday on `"global"`.

The rules for picking an assignment's calendar:

- The resource has a calendar and the task doesn't: the resource's calendar.
- Both have one: their intersection. Give a task a `mornings` calendar and assign it to Dana, and the work runs Tuesday to Friday mornings only.
- The resource has none: the task's calendar.
- Summaries and milestones always use the task's calendar.

A task with several assignments on different calendars spans the **envelope** of its assignments. Each assignment starts at the first working time of its own calendar and runs the full duration there. The task ends when the last one ends. The reported `duration` stays the working time of each assignment, not the length of the envelope. Results don't include per-assignment dates.

Resource calendars also shape resource load: each assignment books work only on its own calendar. See [Resource load](<https://docs.dhtmlx.com/planning-engine/java/resources-and-assignment/resource-load.md>).

## Flat time

Set [`config.useCalendars`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md#usecalendars>) to `false` to schedule without working hours. Every calendar becomes 24/7, so a day of duration is 24 hours:

```java
import org.dhtmlx.plen.*;
import java.util.List;

public class FlatTime {
    public static void main(String[] args) {
        Moment friday = Moment.fromYmdHm(2026, 8, 14, 8, 0);
        if (friday == null) {
            throw new IllegalArgumentException("invalid date");
        }

        ProjectData data = new ProjectData()
            .withConfig(new SchedulingConfig().withUseCalendars(false))
            .withTasks(List.of(
                new TaskData(Id.str("burn-in")).withStartDate(friday).withDuration(2.0))); // days

        ScheduleResult result = Engine.schedule(data, new SolveOptions().withIncludeUnchanged(true));
        for (Mutation update : result.updates) {
            System.out.println(update.id + ": " + update.startDate + " -> " + update.endDate);
        }
    }
}
```

Output:

```text
burn-in: 2026-08-14 08:00 -> 2026-08-16 08:00
```

With calendars on, the same task skips the weekend and ends on Tuesday at 08:00. The engine still validates calendar ids, but they all resolve to the same flat time, so you can toggle `useCalendars` without touching the rest of the data. In flat mode resource and assignment edits never move dates.

## Explicit starts on non-working time

When an edit sets a start that falls on non-working time, the engine doesn't always snap it. A task **with no predecessor** keeps the exact start you gave it. Its end is computed forward on the working calendar, so the duration is still right. A task with a predecessor gets its start from the link instead.

```java
import org.dhtmlx.plen.*;
import java.util.List;

public class OffHoursStart {
    static Moment at(int day, int hour) {
        Moment m = Moment.fromYmdHm(2026, 8, day, hour, 0);
        if (m == null) {
            throw new IllegalArgumentException("invalid date");
        }
        return m;
    }

    public static void main(String[] args) {
        ProjectData data = new ProjectData()
            .withConfig(new SchedulingConfig().withDurationUnit(TimeUnit.HOUR))
            .withTasks(List.of(
                new TaskData(Id.str("deploy")).withStartDate(at(13, 8)).withDuration(4.0))); // Thursday

        Project project = new Project();
        project.solve(data, new SolveOptions());

        // The user drags the task onto Saturday 10:00.
        SchedulingChanges changes = new SchedulingChanges();
        changes.tasks.update.add(new TaskUpdate(Id.str("deploy")).withStartDate(Patch.of(at(15, 10))));
        project.apply(changes);

        ScheduleResult result = project.solve(null, new SolveOptions());
        for (Mutation update : result.updates) {
            System.out.println(update.id + ": " + update.startDate + " -> " + update.endDate);
        }
    }
}
```

Output:

```text
deploy: 2026-08-15 10:00 -> 2026-08-17 12:00
```

The start stays on Saturday 10:00. The four hours of work run Monday 08:00-12:00. This applies to [`Project#apply`](<https://docs.dhtmlx.com/planning-engine/java/api/project-apply.md>) and [`Project#preview`](<https://docs.dhtmlx.com/planning-engine/java/api/project-preview.md>), the calls behind drags in an editor.

This is by design: the UI layer owns snapping. If your editor should only allow working-time starts, snap the date before you send the [`TaskUpdate`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/task-update.md>).
