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


# Links, constraints and summaries

This guide covers the rules that decide where a task lands: dependency links with lag, date constraints, how much free time the scheduler keeps between tasks, how summary tasks move, and what happens when links form a cycle. It's for developers who already [schedule a project](<https://docs.dhtmlx.com/planning-engine/java/scheduling/scheduling-a-project.md>) and now need to control or explain the dates it produces.

## How the scheduler thinks

Every task has a **preferred position**: the dates you sent in [`TaskData`](<https://docs.dhtmlx.com/planning-engine/java/api/data/task-data.md>). Scheduling starts from those dates and applies three kinds of rules on top:

- **Links** push or pull a task relative to another task. A [`LinkData`](<https://docs.dhtmlx.com/planning-engine/java/api/data/link-data.md>) connects a `source` to a `target` with one of four link types and an optional lag. Links are the only thing that ties one task's dates to another's.
- **Constraints** bound a single task to a date (`SNET`, `MFO`, ...) or ask it to sit as early or as late as its links allow (`ASAP`, `ALAP`). They act only when [`applyConstraints`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md#applyconstraints>) is `true`.
- **Gap behaviour** decides what to do with free time. With [`GapBehavior.PRESERVE`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md#gapbehavior>) (the default), a task keeps its preferred dates whenever its links allow them. With `GapBehavior.COMPRESS`, every task is pulled to its earliest possible position.

Summary tasks sit on top of this. A summary's span is always derived from its children, so a summary takes both dates or none, never a duration. When a link points at a summary, the scheduler either moves the whole summary as one block or expands the link onto the summary's leaves. [`moveProjects`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md#moveprojects>) picks the mode.

Some tasks never move. They act as fixed points for everything linked to them:

- tasks with `autoScheduling = false`
- unscheduled tasks (`unscheduled = true`, which also drops their links)
- completed tasks, when `useProgress` is on and `progress == 1.0`

Nothing in this guide throws at scheduling time. A constraint the links can't satisfy becomes a [`Conflict`](<https://docs.dhtmlx.com/planning-engine/java/api/results/conflict.md>). A link that can't apply becomes a [`Note.DroppedLink`](<https://docs.dhtmlx.com/planning-engine/java/api/results/note.md#droppedlink>). A dependency cycle becomes a [`Cycle`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#cycle>) entry. All three are data in the [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md>). Exceptions come only from invalid input, such as a dated constraint without a date or a non-finite lag: strict validation throws an [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>). See [Reading results](<https://docs.dhtmlx.com/planning-engine/java/scheduling/reading-results.md>) for the full result shape.

To see which rule placed a task, call [`solve`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) with [`SolveOptions#withReasons(true)`](<https://docs.dhtmlx.com/planning-engine/java/api/project/solve-options.md#reasons>). Each update then carries a [`Reason`](<https://docs.dhtmlx.com/planning-engine/java/api/results/reason.md>): `Reason.Asap` (pushed by a predecessor link), `Reason.Alap` (pulled by a successor link), `Reason.SummaryTranslation` (moved with its summary block), `Reason.Constraint` or `Reason.Preferred`.

The examples below use [`Engine.schedule`](<https://docs.dhtmlx.com/planning-engine/java/api/engine-schedule.md>), a one-shot `new Project().solve(data, options)`. The same rules apply to a long-lived [`Project`](<https://docs.dhtmlx.com/planning-engine/java/api/project/project.md>).

## Link types and lag

A link's type names which end of the source drives which end of the target:

| [`LinkType`](<https://docs.dhtmlx.com/planning-engine/java/api/data/link-data.md#linktype>) | The target's... | ...follows the source's... |
|---|---|---|
| `FS` (default) | start | finish |
| `SS` | start | start |
| `FF` | finish | finish |
| `SF` | finish | start |

`lag` adds working time between the two ends. It's counted on the target task's calendar, in [`durationUnit`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md#durationunit>) unless the link sets its own `lagUnit`. See [Working time and calendars](<https://docs.dhtmlx.com/planning-engine/java/scheduling/working-time-calendars.md>) for how calendars define working time.

### Chaining tasks with lag

Build each link with `new LinkData()` and the `withX` setters. Fields you don't set keep their defaults: `FS`, zero lag, no `lagUnit`.

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

public class LinkLag {
  public static void main(String[] args) throws Exception {
    Moment monday = Moment.fromYmdHm(2026, 3, 2, 0, 0);

    ProjectData data =
        new ProjectData()
            .withTasks(
                List.of(
                    new TaskData(Id.num(1)).withStartDate(monday).withDuration(3.0),
                    new TaskData(Id.num(2)).withStartDate(monday).withDuration(2.0),
                    new TaskData(Id.num(3)).withStartDate(monday).withDuration(2.0)))
            .withLinks(
                List.of(
                    // Task 2 starts 2 working days after task 1 finishes.
                    new LinkData()
                        .withId(Id.num(1))
                        .withSource(Id.num(1))
                        .withTarget(Id.num(2))
                        .withLag(2.0),
                    // Task 3 starts 1 working day after task 1 starts.
                    new LinkData()
                        .withId(Id.num(2))
                        .withSource(Id.num(1))
                        .withTarget(Id.num(3))
                        .withLinkType(LinkType.SS)
                        .withLag(1.0)));

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

Output:

```
task 2: 2026-03-09 00:00 .. 2026-03-11 00:00
task 3: 2026-03-03 00:00 .. 2026-03-05 00:00
```

Task 1 works Monday to Wednesday. The FS lag of two working days covers Thursday and Friday, so task 2 starts the next Monday. Task 1 isn't listed because it didn't move: [`updates`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#updates>) holds only changed tasks.

### Lag in a different unit

A project measured in days can still have a link whose lag is in hours. Set [`lagUnit`](<https://docs.dhtmlx.com/planning-engine/java/api/data/link-data.md#lagunit>) on that link:

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

public class LagUnit {
  public static void main(String[] args) throws Exception {
    Moment monday = Moment.fromYmdHm(2026, 3, 2, 0, 0);

    ProjectData data =
        new ProjectData()
            .withTasks(
                List.of(
                    new TaskData(Id.str("pour")).withStartDate(monday).withDuration(1.0),
                    new TaskData(Id.str("strip")).withStartDate(monday).withDuration(1.0)))
            .withLinks(
                List.of(
                    new LinkData()
                        .withId(Id.str("cure"))
                        .withSource(Id.str("pour"))
                        .withTarget(Id.str("strip"))
                        .withLag(4.0)
                        .withLagUnit(TimeUnit.HOUR)));

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

Output:

```
strip: 2026-03-03 12:00 .. 2026-03-04 12:00
```

`strip` starts four working hours after `pour` ends on Monday evening, on the default 08:00-12:00, 13:00-17:00 calendar.

:::note
Without a `lagUnit`, lag is measured in the project's `durationUnit`.
:::

### Links that get dropped

A link that can't apply is skipped and reported as a [`Note.DroppedLink`](<https://docs.dhtmlx.com/planning-engine/java/api/results/note.md#droppedlink>) with a reason. The rest of the plan still runs. This happens when:

- the source or target id doesn't exist, or names an unscheduled task
- the link connects a summary to its own descendant and [`descendantLinks`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md#descendantlinks>) is `false` (the default)
- an endpoint is a summary with `autoScheduling = false` and summaries expand to leaves (see [Summary tasks](<https://docs.dhtmlx.com/planning-engine/java/scheduling/links-constraints-summaries.md#summary-tasks>))

A `lag` that isn't a finite number is different: strict validation throws an [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>) with path `links[i].lag`.

## Constraints

A constraint is set per task with `constraintType` and, for the six dated types, `constraintDate`:

| [`ConstraintType`](<https://docs.dhtmlx.com/planning-engine/java/api/data/task-data.md#constrainttype>) | Meaning | Needs a date |
|---|---|---|
| `ASAP` | as soon as the links allow | no |
| `ALAP` | as late as the links allow | no |
| `SNET` / `SNLT` | start no earlier / no later than | yes |
| `FNET` / `FNLT` | finish no earlier / no later than | yes |
| `MSO` / `MFO` | must start on / must finish on | yes |

Constraints are hard bounds, and they exist only when `applyConstraints` is `true`. With the default `false`, the scheduler ignores `constraintType` and `constraintDate` completely, so even an `MSO` pin has no effect. A dated constraint without `constraintDate` is invalid input in strict mode, whatever `applyConstraints` says.

### Holding a task back with a dated constraint

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

public class Snet {
  public static void main(String[] args) throws Exception {
    Moment monday = Moment.fromYmdHm(2026, 3, 2, 0, 0);
    Moment thursday = Moment.fromYmdHm(2026, 3, 5, 0, 0);

    ProjectData data =
        new ProjectData()
            .withConfig(new SchedulingConfig().withApplyConstraints(true))
            .withTasks(
                List.of(
                    new TaskData(Id.str("install"))
                        .withStartDate(monday)
                        .withDuration(2.0)
                        .withConstraintType(ConstraintType.SNET)
                        .withConstraintDate(thursday)));

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

Output:

```
install: 2026-03-05 00:00 .. 2026-03-07 00:00 (Constraint{})
```

`install` moves from Monday to Thursday with [`Reason.Constraint`](<https://docs.dhtmlx.com/planning-engine/java/api/results/reason.md>). The end is Saturday 00:00, which is the close of Friday in day units.

### Detecting a constraint that a link breaks

When a link and a hard constraint disagree, the constraint wins. The task keeps the constrained date, and the result reports the clash as a [`Conflict.ConstraintViolation`](<https://docs.dhtmlx.com/planning-engine/java/api/results/conflict.md#constraintviolation>):

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

public class ConstraintClash {
  public static void main(String[] args) throws Exception {
    Moment monday = Moment.fromYmdHm(2026, 3, 2, 0, 0);
    Moment tuesday = Moment.fromYmdHm(2026, 3, 3, 0, 0);

    ProjectData data =
        new ProjectData()
            .withConfig(new SchedulingConfig().withApplyConstraints(true))
            .withTasks(
                List.of(
                    new TaskData(Id.str("build")).withStartDate(monday).withDuration(3.0),
                    new TaskData(Id.str("demo"))
                        .withStartDate(tuesday)
                        .withDuration(1.0)
                        .withConstraintType(ConstraintType.MSO)
                        .withConstraintDate(tuesday)))
            .withLinks(
                List.of(
                    new LinkData()
                        .withId(Id.num(1))
                        .withSource(Id.str("build"))
                        .withTarget(Id.str("demo"))));

    ScheduleResult result = Engine.schedule(data, new SolveOptions());
    for (Conflict conflict : result.conflicts) {
      if (conflict instanceof Conflict.ConstraintViolation v) {
        System.out.println(
            v.task + ": " + v.constraint + " " + v.violation
                + ", required " + v.required + ", actual " + v.actual);
      }
    }
  }
}
```

Output:

```
demo: MSO LATEST_START, required 2026-03-03 00:00, actual 2026-03-05 00:00
```

`build` finishes on Wednesday, so the link wants `demo` to start on Thursday. The `MSO` pin keeps it on Tuesday, and `updates` is empty. The [`Violation`](<https://docs.dhtmlx.com/planning-engine/java/api/results/conflict.md#violation>) says which bound broke: here the latest allowed start.

### Placing a task as late as possible

`ALAP` moves a task as close to its successors as the links allow, even in a forward-scheduled project:

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

public class Alap {
  public static void main(String[] args) throws Exception {
    Moment monday = Moment.fromYmdHm(2026, 3, 2, 0, 0);
    Moment friday = Moment.fromYmdHm(2026, 3, 6, 0, 0);

    ProjectData data =
        new ProjectData()
            .withConfig(new SchedulingConfig().withApplyConstraints(true))
            .withTasks(
                List.of(
                    new TaskData(Id.str("order-food"))
                        .withStartDate(monday)
                        .withDuration(1.0)
                        .withConstraintType(ConstraintType.ALAP),
                    new TaskData(Id.str("party"))
                        .withStartDate(friday)
                        .withDuration(1.0)
                        .withConstraintType(ConstraintType.MSO)
                        .withConstraintDate(friday)))
            .withLinks(
                List.of(
                    new LinkData()
                        .withId(Id.num(1))
                        .withSource(Id.str("order-food"))
                        .withTarget(Id.str("party"))));

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

Output:

```
order-food: 2026-03-05 00:00 .. 2026-03-06 00:00
```

`order-food` moves from Monday to Thursday, the last day before `party`. To plan a whole project backward from a deadline, set [`scheduleFromEnd`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md#schedulefromend>) and `projectEnd` instead.

### Inheriting a summary's constraint

With [`projectConstraint`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md#projectconstraint>) on, a task without its own constraint inherits the nearest ancestor's:

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

public class InheritConstraint {
  public static void main(String[] args) throws Exception {
    Moment monday = Moment.fromYmdHm(2026, 3, 2, 0, 0);
    Moment wednesday = Moment.fromYmdHm(2026, 3, 4, 0, 0);

    ProjectData data =
        new ProjectData()
            .withConfig(
                new SchedulingConfig().withApplyConstraints(true).withProjectConstraint(true))
            .withTasks(
                List.of(
                    new TaskData(Id.str("phase"))
                        .withTaskType(TaskType.SUMMARY)
                        .withConstraintType(ConstraintType.SNET)
                        .withConstraintDate(wednesday),
                    new TaskData(Id.str("step"))
                        .withParent(Id.str("phase"))
                        .withStartDate(monday)
                        .withDuration(1.0)));

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

Output:

```
step (TASK): 2026-03-04 00:00 .. 2026-03-05 00:00
phase (SUMMARY): 2026-03-04 00:00 .. 2026-03-05 00:00
```

`step` moves to Wednesday, and the rolled-up `phase` follows it as a [`MutationKind.SUMMARY`](<https://docs.dhtmlx.com/planning-engine/java/api/results/mutation.md#mutationkind>) update. With `projectConstraint` off, nothing in this example moves. [`groupMode`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md#groupmode>) turns inheritance off again. That's the only thing `groupMode` does.

## Gap behaviour

[`gapBehavior`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md#gapbehavior>) decides what happens to the free time between linked tasks:

- `GapBehavior.PRESERVE` (default) keeps each task at its preferred dates as long as its links allow them. A link pushes a task only when the task would otherwise start too early.
- `GapBehavior.COMPRESS` pulls every task to the earliest position its links and constraints allow, closing the gaps.

### Keeping or closing a gap

The same data, scheduled with both settings:

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

public class Gaps {
  static void plan(GapBehavior gapBehavior) {
    Moment monday = Moment.fromYmdHm(2026, 3, 2, 0, 0);
    Moment thursday = Moment.fromYmdHm(2026, 3, 5, 0, 0);

    ProjectData data =
        new ProjectData()
            .withConfig(new SchedulingConfig().withGapBehavior(gapBehavior))
            .withTasks(
                List.of(
                    new TaskData(Id.str("design")).withStartDate(monday).withDuration(1.0),
                    new TaskData(Id.str("review")).withStartDate(thursday).withDuration(1.0)))
            .withLinks(
                List.of(
                    new LinkData()
                        .withId(Id.num(1))
                        .withSource(Id.str("design"))
                        .withTarget(Id.str("review"))));

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

  public static void main(String[] args) throws Exception {
    plan(GapBehavior.PRESERVE);
    plan(GapBehavior.COMPRESS);
  }
}
```

Output:

```
PRESERVE design: starts 2026-03-02 00:00
PRESERVE review: starts 2026-03-05 00:00
COMPRESS design: starts 2026-03-02 00:00
COMPRESS review: starts 2026-03-03 00:00
```

[`includeUnchanged`](<https://docs.dhtmlx.com/planning-engine/java/api/project/solve-options.md#includeunchanged>) lists every task, including the ones that didn't move. Under `PRESERVE`, `review` stays on Thursday because the link only requires it to start after Monday. Under `COMPRESS`, it moves to Tuesday.

`COMPRESS` also changes how summaries behave: it always expands summary links onto leaves, as the next section describes.

## Summary tasks

A summary ([`TaskType.SUMMARY`](<https://docs.dhtmlx.com/planning-engine/java/api/data/task-data.md#tasktype>), or `"project"` in JSON) groups child tasks through their `parent` field. Its span is always rolled up from its children. Changed summaries appear in `updates` with `kind == MutationKind.SUMMARY`. Write them back to your model like any other update.

`moveProjects` picks one of two modes for links that touch a summary:

- **Block mode** (`moveProjects = true`, the default, together with `GapBehavior.PRESERVE`): the summary moves as one block. A link into the summary shifts the whole block, and every child keeps its offset inside it.
- **Leaf expansion** (`moveProjects = false`, or any project with `GapBehavior.COMPRESS`): the link is expanded onto the summary's leaf tasks, and each leaf is scheduled on its own.

Inside a moving block, some children stay put: a child pinned by a dated constraint, a child with `autoScheduling = false`, and, with [`useProgress`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md#useprogress>), a completed child. A summary with `autoScheduling = false` is a fixed boundary that keeps its explicit dates. In leaf-expansion mode its links are dropped with a [`Note.DroppedLink`](<https://docs.dhtmlx.com/planning-engine/java/api/results/note.md#droppedlink>).

### Moving a summary as a block

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

public class SummaryBlock {
  static void plan(boolean moveProjects) {
    Moment monday = Moment.fromYmdHm(2026, 3, 2, 0, 0);
    Moment tuesday = Moment.fromYmdHm(2026, 3, 3, 0, 0);

    ProjectData data =
        new ProjectData()
            .withConfig(new SchedulingConfig().withMoveProjects(moveProjects))
            .withTasks(
                List.of(
                    new TaskData(Id.str("prep")).withStartDate(monday).withDuration(3.0),
                    new TaskData(Id.str("phase")).withTaskType(TaskType.SUMMARY),
                    new TaskData(Id.str("a"))
                        .withParent(Id.str("phase"))
                        .withStartDate(monday)
                        .withDuration(1.0),
                    new TaskData(Id.str("b"))
                        .withParent(Id.str("phase"))
                        .withStartDate(tuesday)
                        .withDuration(1.0)))
            .withLinks(
                List.of(
                    new LinkData()
                        .withId(Id.num(1))
                        .withSource(Id.str("prep"))
                        .withTarget(Id.str("phase"))));

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

  public static void main(String[] args) throws Exception {
    plan(true);
    plan(false);
  }
}
```

Output:

```
moveProjects = true
  a TASK: 2026-03-05 00:00 .. 2026-03-06 00:00 SummaryTranslation{summaryId=phase}
  b TASK: 2026-03-06 00:00 .. 2026-03-07 00:00 SummaryTranslation{summaryId=phase}
  phase SUMMARY: 2026-03-05 00:00 .. 2026-03-07 00:00 Asap{linkId=1, sourceId=prep}
moveProjects = false
  phase SUMMARY: 2026-03-05 00:00 .. 2026-03-06 00:00 Preferred{}
  a TASK: 2026-03-05 00:00 .. 2026-03-06 00:00 Asap{linkId=1, sourceId=prep}
  b TASK: 2026-03-05 00:00 .. 2026-03-06 00:00 Asap{linkId=1, sourceId=prep}
```

The `phase` summary has no dates of its own, so its span comes from `a` and `b`. In block mode the link pushes `phase` to Thursday, and `a` and `b` move with it, one day apart as before. In leaf-expansion mode the link lands on each leaf: both `a` and `b` start on Thursday with `Reason.Asap`, and `phase` shrinks to one day.

### Keeping completed work in place

With `useProgress`, a child with `progress == 1.0` is completed and doesn't move with its block:

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

public class CompletedChild {
  public static void main(String[] args) throws Exception {
    Moment monday = Moment.fromYmdHm(2026, 3, 2, 0, 0);
    Moment tuesday = Moment.fromYmdHm(2026, 3, 3, 0, 0);

    ProjectData data =
        new ProjectData()
            .withConfig(new SchedulingConfig().withUseProgress(true))
            .withTasks(
                List.of(
                    new TaskData(Id.str("prep")).withStartDate(monday).withDuration(3.0),
                    new TaskData(Id.str("phase")).withTaskType(TaskType.SUMMARY),
                    new TaskData(Id.str("done"))
                        .withParent(Id.str("phase"))
                        .withStartDate(monday)
                        .withDuration(1.0)
                        .withProgress(1.0),
                    new TaskData(Id.str("open"))
                        .withParent(Id.str("phase"))
                        .withStartDate(tuesday)
                        .withDuration(1.0)))
            .withLinks(
                List.of(
                    new LinkData()
                        .withId(Id.num(1))
                        .withSource(Id.str("prep"))
                        .withTarget(Id.str("phase"))));

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

Output:

```
open: 2026-03-06 00:00 .. 2026-03-07 00:00
phase: null .. 2026-03-07 00:00
```

Only `open` moves, to Friday. `done` stays on Monday, so it isn't listed, and the summary stretches to cover both. Its start didn't change, which is why `phase` reports only a new end: a [`Mutation`](<https://docs.dhtmlx.com/planning-engine/java/api/results/mutation.md>) sets the changed fields and leaves the rest `null`.

### Linking a summary to its own children

By default, a link between a summary and one of its descendants is dropped:

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

public class DescendantLink {
  public static void main(String[] args) throws Exception {
    Moment monday = Moment.fromYmdHm(2026, 3, 2, 0, 0);

    ProjectData data =
        new ProjectData()
            .withTasks(
                List.of(
                    new TaskData(Id.str("phase")).withTaskType(TaskType.SUMMARY),
                    new TaskData(Id.str("kickoff"))
                        .withParent(Id.str("phase"))
                        .withStartDate(monday)
                        .withDuration(1.0)))
            .withLinks(
                List.of(
                    new LinkData()
                        .withId(Id.str("l1"))
                        .withSource(Id.str("phase"))
                        .withTarget(Id.str("kickoff"))
                        .withLinkType(LinkType.SS)));

    ScheduleResult result = Engine.schedule(data, new SolveOptions());
    for (Note note : result.notes) {
      if (note instanceof Note.DroppedLink dropped) {
        System.out.println("link " + dropped.link + " dropped: " + dropped.reason);
      }
    }
  }
}
```

Output:

```
link l1 dropped: link between a summary and its own descendant (descendant_links disabled)
```

Set [`descendantLinks`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md#descendantlinks>) to `true` to keep such links.

To reschedule only one summary and the tasks that depend on it, see [Reschedule a branch](<https://docs.dhtmlx.com/planning-engine/java/how-to/reschedule-a-branch.md>).

## Cycles

A dependency cycle (A waits for B, B waits for A) has no valid schedule. Planning Engine doesn't treat it as an error. A cyclic run:

- leaves every date unchanged and returns empty `updates`
- lists each unique simple cycle in [`cycles`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#cycles>) as a [`Cycle`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#cycle>) with its `tasks` and `links`
- returns analytics with status [`AnalyticsStatus.INVALID`](<https://docs.dhtmlx.com/planning-engine/java/api/results/analytics.md#analyticsstatus>) if you asked for any (see [`Analytics`](<https://docs.dhtmlx.com/planning-engine/java/api/results/analytics.md>))
- still installs a baseline on a [`Project`](<https://docs.dhtmlx.com/planning-engine/java/api/project/project.md>), so you can edit the data to remove the cycle

Cycle detection covers fixed and completed tasks and summary containment too. A self-link is a one-task cycle. A cycle in the `parent` hierarchy is different: it's invalid input, and validation throws an [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>).

### Reporting cycles

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

public class ReportCycles {
  static LinkData link(String id, String source, String target) {
    return new LinkData().withId(Id.str(id)).withSource(Id.str(source)).withTarget(Id.str(target));
  }

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

    ProjectData data =
        new ProjectData()
            .withTasks(
                List.of(
                    new TaskData(Id.str("a")).withStartDate(monday).withDuration(1.0),
                    new TaskData(Id.str("b")).withStartDate(monday).withDuration(1.0),
                    new TaskData(Id.str("c")).withStartDate(monday).withDuration(1.0)))
            .withLinks(List.of(link("ab", "a", "b"), link("ba", "b", "a"), link("bc", "b", "c")));

    Project project = new Project();
    SolveOptions options =
        new SolveOptions().withAnalytics(new AnalyticsOptions().withCriticalPath(true));
    ScheduleResult result = project.solve(data, options);

    System.out.println("updates: " + result.updates.size());
    for (Cycle cycle : result.cycles) {
      System.out.println("cycle: tasks " + cycle.tasks + ", links " + cycle.links);
    }
    if (result.analytics != null) {
      System.out.println("analytics: " + result.analytics.status);
    }
  }
}
```

Output:

```
updates: 0
cycle: tasks [a, b], links [ab, ba]
analytics: INVALID
```

Link `bc` isn't part of the cycle, but `c` doesn't move either: a cyclic run schedules nothing.

### Breaking a cycle

Remove one of the cycle's links with [`Project#apply`](<https://docs.dhtmlx.com/planning-engine/java/api/project-apply.md>), then solve the current state again with `solve(null, ...)`:

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

public class BreakCycle {
  static LinkData link(String id, String source, String target) {
    return new LinkData().withId(Id.str(id)).withSource(Id.str(source)).withTarget(Id.str(target));
  }

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

    ProjectData data =
        new ProjectData()
            .withTasks(
                List.of(
                    new TaskData(Id.str("a")).withStartDate(monday).withDuration(1.0),
                    new TaskData(Id.str("b")).withStartDate(monday).withDuration(1.0)))
            .withLinks(List.of(link("ab", "a", "b"), link("ba", "b", "a")));

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

    if (!result.cycles.isEmpty()) {
      Cycle cycle = result.cycles.get(0);
      Id culprit = cycle.links.get(cycle.links.size() - 1);
      SchedulingChanges changes =
          new SchedulingChanges()
              .withLinks(new Changes<LinkAdd, LinkUpdate>().withRemove(List.of(culprit)));
      project.apply(changes);

      ScheduleResult fixed = project.solve(null, new SolveOptions());
      System.out.println("cycles: " + fixed.cycles.size());
      for (Mutation update : fixed.updates) {
        System.out.println(update.id + " starts " + update.startDate);
      }
    }
  }
}
```

Output:

```
cycles: 0
b starts 2026-03-03 00:00
```

After `ba` is removed, the project is acyclic again and `b` moves to Tuesday, after `a`. You decide which link to remove. The engine only reports the cycle. For more on `apply` and partial edits, see [Editing a project](<https://docs.dhtmlx.com/planning-engine/java/live-projects/editing-a-project.md>).

## See also

- [`LinkData`](<https://docs.dhtmlx.com/planning-engine/java/api/data/link-data.md>) and [`LinkType`](<https://docs.dhtmlx.com/planning-engine/java/api/data/link-data.md#linktype>)
- [`TaskData`](<https://docs.dhtmlx.com/planning-engine/java/api/data/task-data.md>) for `ConstraintType`, and [`Conflict`](<https://docs.dhtmlx.com/planning-engine/java/api/results/conflict.md>)
- [`SchedulingConfig`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md>) for `applyConstraints`, `gapBehavior`, `moveProjects`, `projectConstraint`, `descendantLinks` and `useProgress`
- [Critical path and slack](<https://docs.dhtmlx.com/planning-engine/java/scheduling/critical-path-and-slack.md>)
