[Canonical page](<https://docs.dhtmlx.com/planning-engine/java/records-ids-and-dates/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Records, ids and dates

This guide covers the types you build input from and read results with: records, lists, ids, dates, patch fields, unions and enums. Read it before you map your own models, database rows or API payloads to [`org.dhtmlx.plen`](<https://docs.dhtmlx.com/planning-engine/java/api/overview.md>).

## How the data model works

Everything you pass in and get back is a plain Java object in the `org.dhtmlx.plen` package. Nothing is `AutoCloseable`, nothing does I/O, and the engine keeps its own normalized copy of whatever you hand it.

There are five kinds of types:

- **Records** are mutable classes that extend [`ValueRecord`](<https://docs.dhtmlx.com/planning-engine/java/api/support/value-record.md>). They have public lowerCamelCase fields, a default for every field, and a fluent `withX(value)` setter per field: [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/java/api/data/project-data.md>), [`TaskData`](<https://docs.dhtmlx.com/planning-engine/java/api/data/task-data.md>), [`LinkData`](<https://docs.dhtmlx.com/planning-engine/java/api/data/link-data.md>), options classes, change sets and results. List fields are [`Vec<T>`](<https://docs.dhtmlx.com/planning-engine/java/api/support/vec.md>), an `ArrayList<T>`.
- **Ids** ([`Id`](<https://docs.dhtmlx.com/planning-engine/java/api/data/id.md>)) are a number or a string. They compare like JavaScript object keys, so `1` and `"1"` are the same id.
- **Dates** ([`Moment`](<https://docs.dhtmlx.com/planning-engine/java/api/data/moment.md>)) are naive local date-times at minute precision. There is no timezone, no DST and no "now". A `Moment` is a wall-clock reading, stored as minutes since 1970-01-01 00:00.
- **Patch fields** ([`Patch<T>`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/patch.md>)) appear only in update records. Each one is in one of three states: leave the field alone, clear it, or set it.
- **Unions** ([`Reason`](<https://docs.dhtmlx.com/planning-engine/java/api/results/reason.md>), [`Conflict`](<https://docs.dhtmlx.com/planning-engine/java/api/results/conflict.md>), [`Note`](<https://docs.dhtmlx.com/planning-engine/java/api/results/note.md>), [`DayRule`](<https://docs.dhtmlx.com/planning-engine/java/api/data/worktime.md#dayrule>)) are abstract classes with one nested static subclass per case. You read them with `instanceof` patterns or a pattern `switch`.

An optional value is a reference type where `null` means "not given": `Double duration`, `Moment startDate`, `Id parent`. The library doesn't use `Optional<T>`. Durations, lags, capacities and assignment values are `double`. Durations and lags count working time in a [`TimeUnit`](<https://docs.dhtmlx.com/planning-engine/java/api/data/time-unit.md>), while `levelingDelay` and pauses count elapsed time.

Data flows in one direction. [`solve(data, …)`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) deep-copies the `ProjectData` you pass, so later changes to your object don't reach the project. Results flow the other way, but they aren't copies: the project keeps its last result as its baseline and can hand the same object out again. Treat results as read-only, and call [`deepClone()`](<https://docs.dhtmlx.com/planning-engine/java/api/data/deep-cloneable.md#deepclone>) on one before you change it.

## Working with the types

### Building records

Chain the `withX` setters. Records that carry an id also have a constructor that takes it, such as `new TaskData(id)`, and every other field keeps its default:

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

public class BuildRecords {
  public static void main(String[] args) throws Exception {
    var start = Moment.fromYmdHm(2026, 8, 10, 8, 0);

    var data = new ProjectData()
        .withTasks(List.of(
            new TaskData(Id.str("design")).withStartDate(start).withDuration(2.0),
            new TaskData(Id.str("build")).withStartDate(start).withDuration(3.0)))
        .withLinks(List.of(
            new LinkData().withId(Id.str("l1")).withSource(Id.str("design")).withTarget(Id.str("build"))))
        .withResources(List.of(new ResourceData().withId(Id.str("dev"))))
        .withAssignments(List.of(
            new AssignmentData().withTask(Id.str("build")).withResource(Id.str("dev")).withValue(1)));

    var result = Engine.schedule(data, new SolveOptions());
    for (var update : result.updates)
      System.out.println(update.id + ": " + update.startDate + " - " + update.endDate);
    // build: 2026-08-12 08:00 - 2026-08-17 08:00
  }
}
```

Only `build` is listed: the link moved it, and results report only the tasks that changed.

`duration` is a `Double`, so pass `2.0`, not `2`. A plain `int` literal doesn't box to `Double` and the call doesn't compile.

The fields are public, so you can also set them directly: `task.duration = 5.0`. Every list field starts empty and every nested record starts with its defaults, so `data.tasks.add(...)` or `data.config.durationUnit = TimeUnit.HOUR` works without creating anything first. The defaults for each field are on the reference pages, for example [`TaskData`](<https://docs.dhtmlx.com/planning-engine/java/api/data/task-data.md>) and [`SchedulingConfig`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md>).

The compiler doesn't know which fields a record needs. A link needs `id`, `source` and `target`. An assignment needs `task`, `resource` and `value`, and its `id` is optional. Set these yourself.

The engine checks whether the data makes sense when you schedule it: a leaf task needs two of start, end and duration, and ids must be unique. Problems throw an [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>) with a `path` such as `tasks[0].start_date`. Paths use the JSON field names, so `startDate` shows up as `start_date`.

Every public method throws `NullPointerException` when you pass `null` for an argument that isn't documented as nullable.

`toString()` on any record prints its fields, such as `TaskData{id=design, parent=null, taskType=TASK, …}`. It's meant for logs, not for parsing.

### Lists

Every list field is a [`Vec<T>`](<https://docs.dhtmlx.com/planning-engine/java/api/support/vec.md>). `Vec` extends `ArrayList<T>`, so it's a `java.util.List` with all the usual methods. Every setter and constructor that takes a list also accepts any `java.util.Collection`, and copies it into a new `Vec`:

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

public class Lists {
  public static void main(String[] args) throws Exception {
    var task = new TaskData(Id.str("build"))
        .withPauses(List.of(new Pause(1.0, 0.5)));        // List.of(...) is copied into a Vec
    task.pauses.add(new Pause(3.0, 1.0));                 // so the field stays mutable

    var hours = new DayRule.Hours(List.of(new TimeRange(480, 720)));
    var slot = new Slot(Id.str("s1"), Id.str("build"), List.of(Id.str("dev1"), Id.str("dev2")));

    System.out.println(task.pauses.size());               // 2
    System.out.println(hours.ranges.size());              // 1
    System.out.println(slot.allowed);                     // [dev1, dev2]
  }
}
```

[`Project.evaluate`](<https://docs.dhtmlx.com/planning-engine/java/api/project-evaluate.md>), [`evaluateWith`](<https://docs.dhtmlx.com/planning-engine/java/api/project-evaluate.md#evaluatewith>), [`suggestAssignments`](<https://docs.dhtmlx.com/planning-engine/java/api/project-suggest-assignments.md>) and [`SkillLayer.slots`](<https://docs.dhtmlx.com/planning-engine/java/api/skill-layer-slots.md>) take any `Collection` as well. `Vec` also has helpers such as [`convertAll`](<https://docs.dhtmlx.com/planning-engine/java/api/support/vec.md#convertall-select>) and [`find`](<https://docs.dhtmlx.com/planning-engine/java/api/support/vec.md#find-findindex>), which the engine uses internally. Ordinary `List` methods are enough in your code.

The package also has helper classes named `Set`, `Queue`, `PriorityQueue`, `Array` and `Runtime`. If you write `import java.util.*;` next to `import org.dhtmlx.plen.*;`, these names clash, so import the Planning Engine types you need by name.

### Ids

Create an [`Id`](<https://docs.dhtmlx.com/planning-engine/java/api/data/id.md>) with [`Id.num(...)`](<https://docs.dhtmlx.com/planning-engine/java/api/data/id.md#num>) or [`Id.str(...)`](<https://docs.dhtmlx.com/planning-engine/java/api/data/id.md#str>):

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

public class Ids {
  public static void main(String[] args) throws Exception {
    Id a = Id.num(7);
    Id b = Id.str("7");

    System.out.println(a.equals(b));            // true
    System.out.println(a.isNum());              // true
    System.out.println(b.strValue());           // 7
    System.out.println(Id.num(1.5));            // 1.5
    System.out.println(Id.num(1e21).key());     // 1e+21

    var names = new HashMap<Id, String>();
    names.put(a, "design");
    System.out.println(names.get(b));           // design
  }
}
```

`equals` and `hashCode` follow [`key()`](<https://docs.dhtmlx.com/planning-engine/java/api/data/id.md#key>), the text JavaScript would use for the id as an object key. Ids work as `HashMap` keys, and a task stored as `7` is found by the lookup `"7"`. As a result, `7` and `"7"` in one project are a duplicate id, not two tasks: scheduling them throws an `InputException` at `tasks[1].id`. `0` and `-0.0` both have the key `"0"`.

To get the original value back, check `isNum()` or `isStr()` and read `numValue()` (`Double`) or `strValue()` (`String`). `toString()` returns the key. `Id` is immutable.

Numeric ids are `double`, so integers above 2^53 lose precision. If your database uses 64-bit keys, pass them as strings: `Id.str(Long.toString(row.id))`. Don't use infinite numbers as ids: `key()`, `equals` and `hashCode` throw `IllegalStateException` for them. `Id.str(null)` throws `NullPointerException`.

[`SchedulingConfig.rootId`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md#rootid>) (default `Id.num(0)`) names the virtual root. A task whose `parent` equals it, by key, is a root task, so a `parent` of `Id.str("0")` also counts as no parent. Set `rootId` if your data uses another root id.

### Moments

A [`Moment`](<https://docs.dhtmlx.com/planning-engine/java/api/data/moment.md>) is an immutable class that wraps one `long`, the public field `minutes`. You can create one from date and time fields, from a `LocalDateTime`, from text or from raw minutes:

```java
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import org.dhtmlx.plen.*;

public class Moments {
  public static void main(String[] args) throws Exception {
    Moment maybe = Moment.fromYmdHm(2026, 2, 29, 9, 0);
    System.out.println(maybe == null);                       // true: 2026 isn't a leap year

    var start = Moment.fromYmdHm(2026, 8, 11, 8, 30);
    System.out.println(start);                               // 2026-08-11 08:30
    System.out.println(start.minutes);                       // 29773950
    System.out.println(Moment.parse("2026-08-11T08:30").equals(start)); // true

    var halfHourLater = new Moment(start.minutes + 30);
    System.out.println(halfHourLater.compareTo(start) > 0);  // true

    LocalDateTime forUi = LocalDateTime.ofEpochSecond(start.minutes * 60, 0, ZoneOffset.UTC);
    System.out.println(forUi);                               // 2026-08-11T08:30
    System.out.println(Moment.fromDateTime(forUi).equals(start)); // true
  }
}
```

[`fromYmdHm`](<https://docs.dhtmlx.com/planning-engine/java/api/data/moment.md#fromymdhm>) returns `null` for an invalid date or time, such as hour 24, minute 60 or February 29 in a common year. The examples in these docs pass its result on directly for dates they know are valid. With input you don't control, check for `null` first, because a `null` start date just means "not given".

[`parse`](<https://docs.dhtmlx.com/planning-engine/java/api/data/moment.md#parse>) accepts `YYYY-M-D`, optionally followed by ` h:m[:s]` or `Th:m[:s]`, and throws `IllegalArgumentException` (`"invalid date"`) for anything else. [`fromDateTime`](<https://docs.dhtmlx.com/planning-engine/java/api/data/moment.md#fromdatetime>) drops seconds and fractions. `Moment` implements `Comparable<Moment>`, so it sorts and works as a `TreeMap` key, and `equals` compares `minutes`. `toString()` gives the `YYYY-MM-DD HH:MM` form.

`Moment` has no method that returns a `LocalDateTime`. To get one, read `minutes` and treat it as an offset from 1970-01-01 00:00, as `LocalDateTime.ofEpochSecond(minutes * 60, 0, ZoneOffset.UTC)` does above. The `UTC` offset there only turns the count back into wall-clock fields. It doesn't make the moment a UTC time.

Adding minutes to `minutes` moves a moment in wall-clock time. It doesn't respect calendars, so leave date math in working time to the scheduler.

A `LocalDateTime` carries no zone, and `fromDateTime` reads its fields as they are. If your times arrive in UTC or another zone, convert them to the zone the plan is in first:

```java
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import org.dhtmlx.plen.*;

public class TimeZones {
  public static void main(String[] args) throws Exception {
    var utc = ZonedDateTime.of(2026, 8, 10, 6, 30, 0, 0, ZoneOffset.UTC);
    var local = utc.withZoneSameInstant(ZoneId.of("Europe/Berlin")).toLocalDateTime();

    System.out.println(Moment.fromDateTime(local));          // 2026-08-10 08:30
  }
}
```

Calendar date exceptions in [`Worktime.dates`](<https://docs.dhtmlx.com/planning-engine/java/api/data/worktime.md#dates>) are a sorted map from `Moment` to `DayRule`, keyed by the date's midnight:

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

public class DateExceptions {
  public static void main(String[] args) throws Exception {
    var worktime = new Worktime();
    worktime.dates.put(Moment.fromYmdHm(2026, 12, 24, 0, 0), new DayRule.Off());
    worktime.dates.put(Moment.fromYmdHm(2026, 12, 31, 0, 0),
        new DayRule.Hours(List.of(new TimeRange(480, 720))));

    var calendar = new CalendarData().withId(Id.str("office")).withWorktime(worktime);
  }
}
```

[`TimeRange`](<https://docs.dhtmlx.com/planning-engine/java/api/data/worktime.md#timerange>) holds minutes since midnight, so `480`-`720` is 08:00-12:00. There's more on calendars in [Working-time calendars](<https://docs.dhtmlx.com/planning-engine/java/scheduling/working-time-calendars.md>).

### Patch fields

Update records ([`TaskUpdate`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/task-update.md>), [`LinkUpdate`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/link-update.md>), [`ResourceUpdate`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/resource-update.md>), [`AssignmentUpdate`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/assignment-update.md>)) have a [`Patch<T>`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/patch.md>) for every field except `id`. A patch is in one of three states:

| State | How to write it | Effect |
|---|---|---|
| Missing | [`Patch.missing()`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/patch.md>), the default of every field | the field stays as it is |
| Null | [`Patch.nullValue()`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/patch.md>) | clears an optional field, or restores a scalar field's default |
| Value | [`Patch.of(value)`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/patch.md>) | sets the field |

A new update record changes nothing until you set fields:

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

public class TaskEdit {
  public static void main(String[] args) throws Exception {
    var update = new TaskUpdate(Id.str("build"))
        .withStartDate(Patch.of(Moment.fromYmdHm(2026, 8, 17, 8, 0)))  // set
        .withDuration(Patch.of(4.0))                                     // set
        .withConstraintType(Patch.nullValue())                           // clear
        .withParent(Patch.of(Id.str("phase-2")));                        // set

    var changes = new SchedulingChanges();
    changes.tasks.update.add(update);
  }
}
```

Java infers the patch's type from the setter, so `Patch.nullValue()` and `Patch.of(4.0)` need no type arguments. As with records, numeric patches take boxed values: `Patch.of(4.0)` for a `Patch<Double>`, not `Patch.of(4)`.

`pauses` is a `Patch<Vec<Pause>>`. A value replaces the whole list and `nullValue()` clears it. Clearing a field the record can't exist without, such as a link's `source`, is rejected. [`apply`](<https://docs.dhtmlx.com/planning-engine/java/api/project-apply.md>) reports it in [`ApplyReport.rejected`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/apply-report.md#rejected>) instead of throwing.

`Patch.of(null)` isn't the same as `Patch.nullValue()`. It's a Value patch that holds `null`. On a task's or resource's `calendarId` it clears the calendar. On any other field it throws `NullPointerException` when the change is applied, so use `nullValue()` to clear.

To read a patch, switch on its `kind`, or use [`tryGetValue`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/patch.md#trygetvalue>):

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

public class ReadPatch {
  static String describe(Patch<Double> p) {
    return switch (p.kind) {
      case MISSING -> "unchanged";
      case NULL -> "reset";
      case VALUE -> "set to " + p.value();
    };
  }

  public static void main(String[] args) throws Exception {
    System.out.println(describe(Patch.missing()));    // unchanged
    System.out.println(describe(Patch.nullValue()));  // reset
    System.out.println(describe(Patch.of(2.5)));      // set to 2.5

    var out = new Ref<Double>();
    if (Patch.of(2.5).tryGetValue(out))
      System.out.println(out.value);                  // 2.5
  }
}
```

`value()` throws `IllegalStateException` on a patch that isn't in the Value state. Check `hasValue()` first, or use `tryGetValue` with a [`Ref<T>`](<https://docs.dhtmlx.com/planning-engine/java/api/support/ref.md>) out-holder. `isMissing()` and `isNull()` test the other two states.

A [`Mutation`](<https://docs.dhtmlx.com/planning-engine/java/api/results/mutation.md>) in a result lists only the fields that changed, and they map one-to-one onto a `TaskUpdate`. A changed field becomes a value, and a field that is `null` stays missing:

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

public class MutationToUpdate {
  static TaskUpdate toUpdate(Mutation m) {
    var u = new TaskUpdate(m.id);
    if (m.startDate != null) u.startDate = Patch.of(m.startDate);
    if (m.endDate != null) u.endDate = Patch.of(m.endDate);
    if (m.duration != null) u.duration = Patch.of(m.duration);
    if (m.levelingDelay != null) u.levelingDelay = Patch.of(m.levelingDelay);
    if (m.pauses != null) u.pauses = Patch.of(m.pauses.convertAll(Pause::deepClone));
    return u;
  }
}
```

The pauses are deep-cloned because the result is shared with the project (see [Copies and shared results](<https://docs.dhtmlx.com/planning-engine/java/records-ids-and-dates.md#copies-and-shared-results>)). The full edit workflow is in [Editing a project](<https://docs.dhtmlx.com/planning-engine/java/live-projects/editing-a-project.md>).

### Unions and pattern matching

[`Reason`](<https://docs.dhtmlx.com/planning-engine/java/api/results/reason.md>), [`Conflict`](<https://docs.dhtmlx.com/planning-engine/java/api/results/conflict.md>), [`Note`](<https://docs.dhtmlx.com/planning-engine/java/api/results/note.md>) and [`DayRule`](<https://docs.dhtmlx.com/planning-engine/java/api/data/worktime.md#dayrule>) are abstract classes, and their nested classes are the cases. Match them with a pattern `switch` or `instanceof`, and read the case's fields after the match:

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

public class Unions {
  public static void main(String[] args) throws Exception {
    var start = Moment.fromYmdHm(2026, 8, 10, 8, 0);
    var data = new ProjectData()
        .withTasks(List.of(
            new TaskData(Id.str("design")).withStartDate(start).withDuration(2.0),
            new TaskData(Id.str("build")).withStartDate(start).withDuration(3.0)))
        .withLinks(List.of(
            new LinkData().withId(Id.str("l1")).withSource(Id.str("design")).withTarget(Id.str("build"))));

    var result = Engine.schedule(data, new SolveOptions().withReasons(true));

    for (var conflict : result.conflicts) {
      String text = switch (conflict) {
        case Conflict.ConstraintViolation v ->
            v.task + ": " + v.constraint + " wants " + v.required + ", planned " + v.actual;
        case Conflict.CalendarNonWorking c ->
            c.task + ": " + c.proposed + " is non-working, moved to " + c.snapped;
        default -> conflict.toString();
      };
      System.out.println(text);
    }

    for (var update : result.updates) {
      String why = switch (update.reason) {
        case Reason.Asap r -> "pushed by " + r.sourceId + " through link " + r.linkId;
        case Reason.Alap r -> "pulled by " + r.targetId + " through link " + r.linkId;
        case Reason.SummaryTranslation r -> "moved with summary " + r.summaryId;
        case Reason.Constraint r -> "placed by its constraint";
        case Reason.Preferred r -> "kept at its preferred date";
        case null -> "no reason requested";
        default -> update.reason.toString();
      };
      System.out.println(update.id + ": " + why);
    }
    // build: pushed by design through link l1
  }
}
```

The unions aren't `sealed` and the cases aren't Java `record`s. So the compiler can't check that a `switch` covers every case, and you can't use record deconstruction patterns. Keep a `default` branch and read the fields from the matched variable. `Reason` is `null` unless you set [`reasons`](<https://docs.dhtmlx.com/planning-engine/java/api/project/solve-options.md#reasons>) to `true`, and a pattern `switch` on `null` throws `NullPointerException` unless it has a `case null` branch.

`Note` works the same way. [`Note.Repaired`](<https://docs.dhtmlx.com/planning-engine/java/api/results/note.md#repaired>) carries the `path` and `message` of what [`Validation.REPAIR`](<https://docs.dhtmlx.com/planning-engine/java/api/project/validation.md>) fixed. [`Note.DroppedLink`](<https://docs.dhtmlx.com/planning-engine/java/api/results/note.md#droppedlink>), [`DroppedTask`](<https://docs.dhtmlx.com/planning-engine/java/api/results/note.md#droppedtask>) and [`DroppedAssignment`](<https://docs.dhtmlx.com/planning-engine/java/api/results/note.md#droppedassignment>) carry the id and a `reason` string. To build a `DayRule`, create a case directly: `new DayRule.Off()`, `new DayRule.On()` or `new DayRule.Hours(ranges)`.

Union cases compare by value, and the ids inside them compare by key. Two [`CalendarNonWorking`](<https://docs.dhtmlx.com/planning-engine/java/api/results/conflict.md#calendarnonworking>) conflicts for task `Id.num(1)` and task `Id.str("1")` with the same dates are equal.

### Enums

Option values are plain Java enums with `UPPER_SNAKE` constants. The Default column shows what a new record or config uses when you don't set the field:

| Enum | Values | Default |
|---|---|---|
| [`TimeUnit`](<https://docs.dhtmlx.com/planning-engine/java/api/data/time-unit.md>) | `MINUTE`, `HOUR`, `DAY` | `DAY` ([`SchedulingConfig.durationUnit`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md#durationunit>)) |
| [`TaskType`](<https://docs.dhtmlx.com/planning-engine/java/api/data/task-data.md#tasktype>) | `TASK`, `SUMMARY`, `MILESTONE` | `TASK` |
| [`LinkType`](<https://docs.dhtmlx.com/planning-engine/java/api/data/link-data.md#linktype>) | `FS`, `SS`, `FF`, `SF` | `FS` |
| [`ConstraintType`](<https://docs.dhtmlx.com/planning-engine/java/api/data/task-data.md#constrainttype>) | `ASAP`, `ALAP`, `SNET`, `SNLT`, `FNET`, `FNLT`, `MSO`, `MFO` | none (`TaskData.constraintType` is `null`) |
| [`CalendarMode`](<https://docs.dhtmlx.com/planning-engine/java/api/data/calendar-mode.md>) | `RESOURCE`, `TASK`, `TASK_INHERITED` | `RESOURCE` |
| [`GapBehavior`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md#gapbehavior>) | `PRESERVE`, `COMPRESS` | `PRESERVE` |
| [`Validation`](<https://docs.dhtmlx.com/planning-engine/java/api/project/validation.md>) | `STRICT`, `REPAIR` | `STRICT` |

`TimeUnit` has no week, month or year. Express longer durations in days.

The constraint types other than `ASAP` and `ALAP` are dated, so a task that uses one also needs a `constraintDate`. They apply only when [`SchedulingConfig.applyConstraints`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md#applyconstraints>) is `true`.

`toString()` and `name()` give the Java name (`"FS"`, `"TASK_INHERITED"`), not the JSON spelling (`"fs"`, `"task-inherited"`). [`Decode`](<https://docs.dhtmlx.com/planning-engine/java/api/json/decode.md>) and [`Encode`](<https://docs.dhtmlx.com/planning-engine/java/api/json/encode.md>) handle the JSON spellings. If you map your own strings, write the mapping yourself:

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

public class NumericLinkTypes {
  static LinkType parseNumericLinkType(String type) {
    return switch (type) {
      case "0" -> LinkType.FS;
      case "1" -> LinkType.SS;
      case "2" -> LinkType.FF;
      case "3" -> LinkType.SF;
      default -> throw new IllegalArgumentException("unknown link type " + type);
    };
  }
}
```

### Copies and shared results

Every record has two copy methods:

- `copy()` is shallow. The copy is a new object, but its lists and nested records are the same instances as the original's.
- `deepClone()`, from [`DeepCloneable<T>`](<https://docs.dhtmlx.com/planning-engine/java/api/data/deep-cloneable.md>), returns a copy that shares no mutable list or nested record with the original. Most records implement it. The `Reason`, `Conflict` and `Note` cases and `TimeRange` hold only immutable values and have just `copy()`.

`Id` and `Moment` are immutable, so copies share them safely. Use `deepClone()` when you want to edit either side:

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

public class Copies {
  public static void main(String[] args) throws Exception {
    var start = Moment.fromYmdHm(2026, 8, 10, 8, 0);
    var data = new ProjectData()
        .withTasks(List.of(
            new TaskData(Id.str("design")).withStartDate(start).withDuration(2.0),
            new TaskData(Id.str("build")).withStartDate(start).withDuration(3.0)));

    var variant = data.deepClone();
    variant.tasks.get(0).duration = 5.0;
    variant.tasks.add(new TaskData(Id.str("test")).withStartDate(start).withDuration(1.0));

    System.out.println(data.tasks.get(0).duration);  // 2.0
    System.out.println(data.tasks.size());           // 2

    var shallow = data.copy();
    shallow.tasks.add(new TaskData(Id.str("review")));
    System.out.println(data.tasks.size());           // 3: the list is shared
  }
}
```

You don't need to clone before solving: [`solve(data, …)`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) already copies the data, and the project never holds your object.

Results are the opposite case. `solve(null, …)` with no pending edits and equal options returns the cached result, the same instance. Copies of the project share it too, and [`evaluate`](<https://docs.dhtmlx.com/planning-engine/java/api/project-evaluate.md>) and [`suggestAssignments`](<https://docs.dhtmlx.com/planning-engine/java/api/project-suggest-assignments.md>) read it as the baseline. Changing it in place would change what the project sees, so clone it first:

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

public class SharedResults {
  public static void main(String[] args) throws Exception {
    var start = Moment.fromYmdHm(2026, 8, 10, 8, 0);
    var data = new ProjectData()
        .withTasks(List.of(new TaskData(Id.str("design")).withStartDate(start).withDuration(2.0)));

    var project = new Project();
    var first = project.solve(data, new SolveOptions());
    var again = project.solve(null, new SolveOptions());

    System.out.println(first == again);              // true

    var mine = first.deepClone();
    mine.updates.removeIf(u -> u.kind == MutationKind.SUMMARY); // safe: a private copy
  }
}
```

[`LevelingResult.deepClone()`](<https://docs.dhtmlx.com/planning-engine/java/api/resources/leveling-result.md#deepclone>) copies the leveling result but still shares its [`schedule`](<https://docs.dhtmlx.com/planning-engine/java/api/resources/leveling-result.md#schedule>). Call `deepClone()` on `schedule` too if you want to edit it.

Records compare by value. [`ValueRecord`](<https://docs.dhtmlx.com/planning-engine/java/api/support/value-record.md>) gives every record an `equals` and `hashCode` over its public fields, lists element by element. That covers input records, change sets, options and results alike, so `data.equals(data.deepClone())` is `true` and you can use `equals` to detect changes. Doubles compare with `Double.equals`, so `NaN` equals `NaN`, but `-0.0` doesn't equal `0.0`. [`Project`](<https://docs.dhtmlx.com/planning-engine/java/api/project/project.md>) is the exception: it compares by identity.
