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


# Reading results

Every scheduling call returns a [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md>). This guide shows how to write the new dates back into your own model, explain why tasks moved, and handle constraint conflicts, dependency cycles and dropped records. It also covers the baseline token, why you shouldn't modify a result, and how to turn one into JSON.

## How a result is organized

[`Engine.schedule`](<https://docs.dhtmlx.com/planning-engine/java/api/engine-schedule.md>), [`Project#solve`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>), [`Project#preview`](<https://docs.dhtmlx.com/planning-engine/java/api/project-preview.md>) and [`Project#analyze`](<https://docs.dhtmlx.com/planning-engine/java/api/project-analyze.md>) all return a `ScheduleResult`. [`Project#level`](<https://docs.dhtmlx.com/planning-engine/java/api/project-level.md>) returns a [`LevelingResult`](<https://docs.dhtmlx.com/planning-engine/java/api/resources/leveling-result.md>) with the `ScheduleResult` in its [`schedule`](<https://docs.dhtmlx.com/planning-engine/java/api/resources/leveling-result.md#schedule>) field. The result's public fields fall into four groups:

| Group | Fields | What it tells you |
|---|---|---|
| Dates | `updates` | The tasks that changed, with their new fields |
| Problems | `conflicts`, `cycles`, `notes`, `converged` | Rules the plan breaks, loops in the links, records left out, whether the planner settled |
| Extras | `analytics`, `overallocations`, `profile` | Critical path and slack, resource overload, load profile. Each one is `null` unless you requested it or it applies |
| Identity | `baselineId` | Which accepted baseline the result belongs to |

Every result follows these rules:

- **Updates are sparse.** `updates` lists only the tasks whose dates changed. Each [`Mutation`](<https://docs.dhtmlx.com/planning-engine/java/api/results/mutation.md>) sets only the fields that changed and leaves the rest `null`. A task that didn't move isn't in the list. Set [`SolveOptions.includeUnchanged`](<https://docs.dhtmlx.com/planning-engine/java/api/project/solve-options.md#includeunchanged>) when you need every task's dates, for example on the first load.
- **Problems are data, not exceptions.** A plan with conflicts, cycles or dropped links still comes back as a normal result. Only invalid input ([`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>)) and calls that don't fit the project's state ([`ProjectException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/project-exception.md>)) throw. See [Error handling](<https://docs.dhtmlx.com/planning-engine/java/error-handling.md>).
- **Order is stable.** The same input and options always produce the same result, in the same order. The one exception is `baselineId`, an opaque token.
- **Updates use the input field names.** A `Mutation` has the same field names as [`TaskData`](<https://docs.dhtmlx.com/planning-engine/java/api/data/task-data.md>) and [`TaskUpdate`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/task-update.md>): `startDate`, `endDate`, `duration`, `levelingDelay`, `pauses`. Each mutation field maps to the `TaskUpdate` field of the same name.
- **Results are shared.** The project can hand the same object to later calls and to copies. Read it, but don't change it. See [Results are shared](<https://docs.dhtmlx.com/planning-engine/java/scheduling/reading-results.md#results-are-shared>).

The variant types `Reason`, `Conflict` and `Note` are abstract classes with nested subclasses. Read them with `instanceof` patterns or a pattern-matching `switch`. The classes aren't `sealed`, so a `switch` over one needs a `default` branch.

## Updates

Read `updates` after each call and copy the changed fields into your model. A `null` field means "unchanged", not "cleared".

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

public class Updates {
  /** The caller's own copy of a task's dates. */
  static final class Row {
    Moment start;
    double duration;

    Row(Moment start, double duration) {
      this.start = start;
      this.duration = duration;
    }
  }

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

    ProjectData data = new ProjectData()
        .withTasks(List.of(
            new TaskData(Id.num(1)).withStartDate(monday).withDuration(2.0),
            new TaskData(Id.num(2)).withStartDate(monday).withDuration(1.0)))
        .withLinks(List.of(
            new LinkData().withId(Id.num(1)).withSource(Id.num(1)).withTarget(Id.num(2))));

    Map<Id, Row> model = new HashMap<>();
    model.put(Id.num(1), new Row(monday, 2.0));
    model.put(Id.num(2), new Row(monday, 1.0));

    ScheduleResult result = Engine.schedule(data, new SolveOptions());
    for (Mutation update : result.updates) {
      System.out.println(update.id + " (" + update.kind + "): start " + update.startDate
          + ", end " + update.endDate + ", duration " + update.duration);

      Row row = model.get(update.id);
      if (row == null) continue;
      if (update.startDate != null) row.start = update.startDate;
      if (update.duration != null) row.duration = update.duration;
    }
  }
}
```

Output:

```
2 (TASK): start 2026-08-12 08:00, end 2026-08-13 08:00, duration null
```

Task 1 isn't listed because it didn't move. Task 2 moved, so it reports a new start and end. Its `duration` is `null` because the working time didn't change. `Id` compares by its normalized key, so it works as a `HashMap` key.

`kind` is a [`MutationKind`](<https://docs.dhtmlx.com/planning-engine/java/api/results/mutation.md#mutationkind>): `TASK`, `SUMMARY` or `MILESTONE`. Summaries appear in `updates` too, because their spans roll up from their children. `levelingDelay` and `pauses` only appear in results from [`Project#level`](<https://docs.dhtmlx.com/planning-engine/java/api/project-level.md>), and only when leveling changed them. [Leveling resources](<https://docs.dhtmlx.com/planning-engine/java/resources-and-assignment/leveling-resources.md>) shows how to write them back.

To get every task's dates instead of only the changed ones, see [Load all task dates](<https://docs.dhtmlx.com/planning-engine/java/how-to/load-all-task-dates.md>).

## Reasons

Turn on [`SolveOptions.reasons`](<https://docs.dhtmlx.com/planning-engine/java/api/project/solve-options.md#reasons>) to learn why each task ended up where it did. Each update then carries a [`Reason`](<https://docs.dhtmlx.com/planning-engine/java/api/results/reason.md>) in its `reason` field:

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

public class Reasons {
  static TaskData task(int id, Moment start) {
    return new TaskData(Id.num(id)).withStartDate(start).withDuration(1.0);
  }

  static LinkData link(int id, int source, int target) {
    return new LinkData().withId(Id.num(id)).withSource(Id.num(source)).withTarget(Id.num(target));
  }

  static String explain(Reason reason) {
    return switch (reason) {
      case Reason.Asap r -> "pushed by task " + r.sourceId + " through link " + r.linkId;
      case Reason.Alap r -> "pulled by task " + 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 recorded";
      default -> "unknown reason";
    };
  }

  public static void main(String[] args) throws Exception {
    Moment monday = Moment.fromYmdHm(2026, 8, 10, 8, 0);
    ProjectData data = new ProjectData()
        .withTasks(List.of(task(1, monday), task(2, monday), task(3, monday)))
        .withLinks(List.of(link(10, 1, 2), link(11, 2, 3)));

    ScheduleResult result = Engine.schedule(data, new SolveOptions().withReasons(true));
    for (Mutation update : result.updates) {
      System.out.println("task " + update.id + ": " + explain(update.reason));
    }
  }
}
```

Output:

```
task 2: pushed by task 1 through link 10
task 3: pushed by task 2 through link 11
```

The subclasses:

- `Reason.Asap` (`linkId`, `sourceId`): a predecessor link pushed the task later.
- `Reason.Alap` (`linkId`, `targetId`): a successor link pulled the task earlier (backward planning).
- `Reason.SummaryTranslation` (`summaryId`): the task moved as part of its summary block.
- `Reason.Constraint`: the task's own constraint placed it.
- `Reason.Preferred`: the task stayed at, or returned to, its own date.

[`PreviewOptions`](<https://docs.dhtmlx.com/planning-engine/java/api/project/preview-options.md>) has the same `reasons` flag, so a drag preview can explain itself too. A cyclic solve reports no reasons because the planner doesn't run, so `reason` stays `null`.

## Conflicts

A conflict is a rule the scheduled plan breaks. It doesn't stop scheduling: the dates are still valid output, and `conflicts` lists what doesn't fit. [`Conflict`](<https://docs.dhtmlx.com/planning-engine/java/api/results/conflict.md>) has two subclasses:

- `Conflict.ConstraintViolation` (`task`, `constraint`, `violation`, `required`, `actual`): a task's constraint and its links disagree. `violation` is a [`Violation`](<https://docs.dhtmlx.com/planning-engine/java/api/results/conflict.md#violation>) that names the bound that broke: `EARLIEST_START`, `LATEST_START`, `EARLIEST_END` or `LATEST_END`.
- `Conflict.CalendarNonWorking` (`task`, `proposed`, `snapped`): with `config.strictCalendar`, a task moved with its summary block landed on non-working time.

Constraints only act when `config.applyConstraints` is `true`. In this example, task 2 must start no later than Tuesday, but its predecessor runs until Wednesday:

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

public class Conflicts {
  public static void main(String[] args) throws Exception {
    Moment monday = Moment.fromYmdHm(2026, 8, 10, 8, 0);
    Moment tuesday = Moment.fromYmdHm(2026, 8, 11, 8, 0);

    TaskData design = new TaskData(Id.num(1)).withStartDate(monday).withDuration(2.0);
    // Build must start no later than Tuesday, but design runs until Wednesday.
    TaskData build = new TaskData(Id.num(2)).withStartDate(monday).withDuration(1.0)
        .withConstraintType(ConstraintType.SNLT)
        .withConstraintDate(tuesday);

    ProjectData data = new ProjectData()
        .withTasks(List.of(design, build))
        .withLinks(List.of(
            new LinkData().withId(Id.num(1)).withSource(Id.num(1)).withTarget(Id.num(2))));
    data.config.applyConstraints = true;

    ScheduleResult result = Engine.schedule(data, new SolveOptions());
    for (Conflict conflict : result.conflicts) {
      if (conflict instanceof Conflict.ConstraintViolation v) {
        System.out.println("task " + v.task + ": " + v.constraint + " violated (" + v.violation
            + "), required " + v.required + ", actual " + v.actual);
      } else if (conflict instanceof Conflict.CalendarNonWorking c) {
        System.out.println("task " + c.task + ": lands on non-working time at " + c.proposed
            + " (snapped " + c.snapped + ")");
      }
    }
    for (Mutation update : result.updates) {
      System.out.println("task " + update.id + ": start " + update.startDate);
    }
  }
}
```

Output:

```
task 2: SNLT violated (LATEST_START), required 2026-08-11 08:00, actual 2026-08-12 08:00
task 2: start 2026-08-11 08:00
```

The upper-bound constraint wins: task 2 starts on Tuesday, at `required`. `actual` is where the link wanted it to start. Show both dates to the user, so they can relax the constraint or move the predecessor. [Links, constraints and summaries](<https://docs.dhtmlx.com/planning-engine/java/scheduling/links-constraints-summaries.md>) explains how each constraint type resolves.

## Cycles and notes

A dependency cycle (task 1 waits for task 2, which waits for task 1) has no valid schedule. Planning Engine doesn't throw on it. It returns the dates unchanged, leaves `updates` empty and lists every unique simple cycle in `cycles`. Each [`Cycle`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#cycle>) holds the task ids (`tasks`) and link ids (`links`) that form the loop.

`notes` lists records the scheduler left out or changed. A [`Note`](<https://docs.dhtmlx.com/planning-engine/java/api/results/note.md>) is one of:

- `Note.DroppedLink` (`link`, `reason`): a link that can't apply, for example one that points to an unscheduled task.
- `Note.DroppedTask` (`task`, `reason`): an unscheduled task.
- `Note.DroppedAssignment` (`assignment`, `reason`): an assignment that doesn't take part.
- `Note.Repaired` (`path`, `message`): a fix applied under [`Validation.REPAIR`](<https://docs.dhtmlx.com/planning-engine/java/api/project/validation.md>).

This example has a cycle between tasks 1 and 2, plus a parked task 3:

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

public class Cycles {
  static TaskData task(int id, Moment start) {
    return new TaskData(Id.num(id)).withStartDate(start).withDuration(1.0);
  }

  static LinkData link(int id, int source, int target) {
    return new LinkData().withId(Id.num(id)).withSource(Id.num(source)).withTarget(Id.num(target));
  }

  public static void main(String[] args) throws Exception {
    Moment monday = Moment.fromYmdHm(2026, 8, 10, 8, 0);
    ProjectData data = new ProjectData()
        .withTasks(List.of(
            task(1, monday),
            task(2, monday),
            // Task 3 is parked: it isn't scheduled, and neither is the link to it.
            task(3, monday).withUnscheduled(true)))
        // 1 -> 2 -> 1 is a cycle.
        .withLinks(List.of(link(10, 1, 2), link(11, 2, 1), link(12, 2, 3)));

    ScheduleResult result = Engine.schedule(data, new SolveOptions());
    for (Cycle cycle : result.cycles) {
      System.out.println("cycle: tasks " + cycle.tasks + ", links " + cycle.links);
    }
    System.out.println("updates: " + result.updates.size());

    for (Note note : result.notes) {
      String line = switch (note) {
        case Note.DroppedLink n -> "link " + n.link + " dropped: " + n.reason;
        case Note.DroppedTask n -> "task " + n.task + " dropped: " + n.reason;
        case Note.DroppedAssignment n -> "assignment " + n.assignment + " dropped: " + n.reason;
        case Note.Repaired n -> "repaired " + n.path + ": " + n.message;
        default -> "note: " + note;
      };
      System.out.println(line);
    }
    if (!result.converged) {
      System.out.println("the planner stopped before a fixed point");
    }
  }
}
```

Output:

```
cycle: tasks [1, 2], links [10, 11]
updates: 0
task 3 dropped: unscheduled
link 12 dropped: target task "3" is unscheduled
```

Highlight the links in `cycle.links` so the user can break the loop. A cyclic solve still installs a baseline. If you requested analytics, they come back with `status` set to [`AnalyticsStatus.INVALID`](<https://docs.dhtmlx.com/planning-engine/java/api/results/analytics.md#analyticsstatus>); see [`Analytics`](<https://docs.dhtmlx.com/planning-engine/java/api/results/analytics.md>).

`converged` is `false` when the planner didn't reach a stable state within `config.maxIterations` (default 8). The dates are still a valid result, so treat it as a warning, not a failure.

To clean up messy input instead of rejecting it, see [Accept messy input](<https://docs.dhtmlx.com/planning-engine/java/how-to/accept-messy-input.md>).

## Baseline token

`baselineId` names the accepted baseline of a [`Project`](<https://docs.dhtmlx.com/planning-engine/java/api/project/project.md>): the dates the last [`solve`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) or [`level`](<https://docs.dhtmlx.com/planning-engine/java/api/project-level.md>) accepted. These calls fill it in:

- `solve` and `level`: the baseline they just accepted.
- [`preview`](<https://docs.dhtmlx.com/planning-engine/java/api/project-preview.md>): the baseline it read.
- [`analyze(null, …)`](<https://docs.dhtmlx.com/planning-engine/java/api/project-analyze.md>) on a solved project with no pending edits: the current baseline. Otherwise `null`.
- [`Engine.schedule`](<https://docs.dhtmlx.com/planning-engine/java/api/engine-schedule.md>): sets it too, but the project behind the call is gone afterwards, so the token has no use.

[`ScheduleResult#acceptedId()`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#acceptedid>) returns `baselineId` and throws `IllegalStateException` when it's `null`. Use it where a missing token is a bug.

The token changes whenever the data changes: new data, an [`apply`](<https://docs.dhtmlx.com/planning-engine/java/api/project-apply.md>), a solve that moves dates, or leveling edits. Keep it next to the dates you display. Pass it back in [`PreviewOptions.baselineId`](<https://docs.dhtmlx.com/planning-engine/java/api/project/preview-options.md#baselineid>) or [`SchedulingChanges.baselineId`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/scheduling-changes.md#baselineid>). If the project moved on in the meantime, the call throws [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/project-exception.md>) with `code == ProjectError.STALE_BASELINE`:

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

public class Token {
  public static void main(String[] args) throws Exception {
    Moment monday = Moment.fromYmdHm(2026, 8, 10, 8, 0);
    ProjectData data = new ProjectData()
        .withTasks(List.of(
            new TaskData(Id.num(1)).withStartDate(monday).withDuration(2.0),
            new TaskData(Id.num(2)).withStartDate(monday).withDuration(1.0)))
        .withLinks(List.of(
            new LinkData().withId(Id.num(1)).withSource(Id.num(1)).withTarget(Id.num(2))));

    Project project = new Project();
    ScheduleResult loaded = project.solve(data, new SolveOptions());
    // Keep the token next to the dates you show.
    String shown = loaded.baselineId;

    SchedulingChanges changes = new SchedulingChanges();
    changes.tasks.update.add(new TaskUpdate(Id.num(1)).withDuration(Patch.of(3.0)));

    PreviewOptions guarded = new PreviewOptions().withBaselineId(shown);
    ScheduleResult preview = project.preview(changes, guarded);
    System.out.println("preview read the shown baseline: " + shown.equals(preview.baselineId));

    // Someone else commits an edit; the accepted baseline changes.
    project.apply(changes);
    ScheduleResult committed = project.solve(null, new SolveOptions());
    System.out.println("token changed: " + !shown.equals(committed.baselineId));

    try {
      project.preview(changes, guarded);
      System.out.println("unexpected: preview accepted a stale token");
    } catch (ProjectException e) {
      if (e.code != ProjectError.STALE_BASELINE) throw e;
      System.out.println("stale: reload the dates before previewing again");
    }
  }
}
```

Output:

```
preview read the shown baseline: true
token changed: true
stale: reload the dates before previewing again
```

The token is opaque and unique to the process. Don't parse it, don't store it as a persistent id, and don't compare tokens across runs or processes: the same data solved twice can get two different tokens. Compare it only with the current one, and always take it from the latest result. [Data updates](<https://docs.dhtmlx.com/planning-engine/java/live-projects/editing-a-project.md>) shows the full edit cycle.

## Results are shared

A `Project` keeps the result of its last `solve` or `level` as part of its baseline. `solve(null, …)` on an unchanged project with equal options returns that same object. Copies made with [`Project#copy()`](<https://docs.dhtmlx.com/planning-engine/java/api/project/project.md#copy>) share it, 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. [`LevelingResult.schedule`](<https://docs.dhtmlx.com/planning-engine/java/api/resources/leveling-result.md#schedule>) is shared the same way.

So treat every result as read-only. If you need to change one, for example to filter `updates` before sending them on, call [`deepClone()`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#deepclone>) first and change the copy:

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

public class Shared {
  public static void main(String[] args) throws Exception {
    Moment monday = Moment.fromYmdHm(2026, 8, 10, 8, 0);
    ProjectData data = new ProjectData()
        .withTasks(List.of(
            new TaskData(Id.num(1)).withStartDate(monday).withDuration(2.0),
            new TaskData(Id.num(2)).withStartDate(monday).withDuration(1.0)))
        .withLinks(List.of(
            new LinkData().withId(Id.num(1)).withSource(Id.num(1)).withTarget(Id.num(2))));

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

    // An unchanged re-solve and a copy hand back the same object.
    System.out.println("re-solve shares it: " + (project.solve(null, new SolveOptions()) == loaded));
    System.out.println("copy shares it: " + (project.copy().solve(null, new SolveOptions()) == loaded));

    // Change a private deep copy, never the shared result.
    ScheduleResult mine = loaded.deepClone();
    mine.updates.clear();
    System.out.println("shared result intact: " + loaded.updates.size() + " update(s)");
  }
}
```

Output:

```
re-solve shares it: true
copy shares it: true
shared result intact: 1 update(s)
```

`copy()` is shallow: the copy shares its `updates` list with the original, so it isn't safe to change. Results compare structurally with `equals`, and `baselineId` is part of the comparison. To compare two results while ignoring the token, `deepClone()` both, set `baselineId = null` on the clones and compare those.

## Results as JSON

[`Encode.text`](<https://docs.dhtmlx.com/planning-engine/java/api/json/encode.md#text>) writes a result as JSON text, for logs or for a client:

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

public class AsJson {
  public static void main(String[] args) throws Exception {
    Moment monday = Moment.fromYmdHm(2026, 8, 10, 8, 0);
    Moment tuesday = Moment.fromYmdHm(2026, 8, 11, 8, 0);

    ProjectData data = new ProjectData()
        .withTasks(List.of(
            new TaskData(Id.num(1)).withStartDate(monday).withDuration(2.0),
            new TaskData(Id.num(2)).withStartDate(monday).withDuration(1.0)
                .withConstraintType(ConstraintType.SNLT)
                .withConstraintDate(tuesday)))
        .withLinks(List.of(
            new LinkData().withId(Id.num(1)).withSource(Id.num(1)).withTarget(Id.num(2))));
    data.config.applyConstraints = true;

    ScheduleResult result = Engine.schedule(data, new SolveOptions().withReasons(true));
    System.out.println(Encode.text(result));
  }
}
```

Output (one line, wrapped here):

```json
{"baselineId":"1-92df7b9db71e9b4b",
 "conflicts":[{"actual":"2026-08-12 08:00","constraint":"snlt","kind":"constraint-violation",
   "required":"2026-08-11 08:00","task":2,"violation":"latest-start"}],
 "converged":true,"cycles":[],"notes":[],
 "updates":[{"end_date":"2026-08-12 08:00","id":2,"kind":"task","reason":{"kind":"constraint"},
   "start_date":"2026-08-11 08:00"}]}
```

The JSON follows these naming rules:

- Result fields are camelCase: `baselineId`, `conflicts`, `criticalPath`.
- Update entries use the snake_case task-data names: `start_date`, `end_date`, `leveling_delay`. A client can send an entry back as a task update.
- `Reason`, `Conflict` and `Note` become objects tagged by `kind` in kebab-case: `"constraint-violation"`, `"summary-translation"`, `"dropped-link"`. Their fields are camelCase (`linkId`, `sourceId`).
- Enums become lowercase or kebab-case strings: `"task"`, `"latest-start"`, `"snlt"`.
- Dates are `"YYYY-MM-DD HH:MM"` strings. Ids stay numbers or strings, as in the input.
- Fields without a value are left out, not written as `null`.

`baselineId` differs from run to run. `Encode.text` throws `IllegalStateException` if the result holds a NaN or infinite number. [Exchange JSON with a client](<https://docs.dhtmlx.com/planning-engine/java/how-to/exchange-json-with-a-client.md>) covers the full JSON round trip.
