[Canonical page](<https://docs.dhtmlx.com/planning-engine/net/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/net/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 set on its [`TaskData`](<https://docs.dhtmlx.com/planning-engine/net/api/core/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/net/api/core/data/link-data.md>) connects a [`Source`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/link-data.md#source-target>) 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 [`Config.ApplyConstraints`](<https://docs.dhtmlx.com/planning-engine/net/api/core/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/net/api/core/data/scheduling-config.md#gapbehavior-1>) (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 always comes 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. [`Config.MoveProjects`](<https://docs.dhtmlx.com/planning-engine/net/api/core/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 [`Config.UseProgress`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#useprogress>) is on and `Progress` is `1`

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

To see which rule placed a task, set [`SolveOptions.Reasons`](<https://docs.dhtmlx.com/planning-engine/net/api/core/project/solve-options.md#reasons>) to `true`. Each update then carries a [`Reason`](<https://docs.dhtmlx.com/planning-engine/net/api/core/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`.

## Link types and lag

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

| [`LinkType`](<https://docs.dhtmlx.com/planning-engine/net/api/core/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`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/link-data.md#lag>) adds working time between the two ends. It's counted on the target task's calendar, in [`Config.DurationUnit`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#durationunit>) unless the link sets its own [`LagUnit`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/link-data.md#lagunit>). See [Working time and calendars](<https://docs.dhtmlx.com/planning-engine/net/scheduling/working-time-calendars.md>) for how calendars define working time.

### Chaining tasks with lag

[`LinkData`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/link-data.md>) needs [`Id`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/link-data.md#id>), `Source` and `Target`. `LinkType` defaults to `Fs` and `Lag` to `0`:

```csharp
using Plen;

var monday = Moment.FromYmdHm(2026, 3, 2, 0, 0)!.Value;

var data = new ProjectData
{
    Tasks =
    [
        new TaskData(1) { StartDate = monday, Duration = 3 },
        new TaskData(2) { StartDate = monday, Duration = 2 },
        new TaskData(3) { StartDate = monday, Duration = 2 },
    ],
    Links =
    [
        // Task 2 starts 2 working days after task 1 finishes.
        new LinkData { Id = 1, Source = 1, Target = 2, LinkType = LinkType.Fs, Lag = 2 },
        // Task 3 starts 1 working day after task 1 starts.
        new LinkData { Id = 2, Source = 1, Target = 3, LinkType = LinkType.Ss, Lag = 1 },
    ],
};

var result = Engine.Schedule(data, new SolveOptions());
foreach (var update in result.Updates)
{
    Console.WriteLine($"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/net/api/core/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/net/api/core/data/link-data.md#lagunit>) on that link:

```csharp
using Plen;

var monday = Moment.FromYmdHm(2026, 3, 2, 0, 0)!.Value;

var data = new ProjectData
{
    Tasks =
    [
        new TaskData("pour") { StartDate = monday, Duration = 1 },
        new TaskData("strip") { StartDate = monday, Duration = 1 },
    ],
    Links =
    [
        new LinkData { Id = "cure", Source = "pour", Target = "strip", Lag = 4, LagUnit = TimeUnit.Hour },
    ],
};

var result = Engine.Schedule(data, new SolveOptions());
foreach (var update in result.Updates)
{
    Console.WriteLine($"{update.Id}: {update.StartDate} .. {update.EndDate}");
}
```

Output:

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

`pour` ends on Monday evening. Four working hours on the default 08:00-12:00, 13:00-17:00 calendar put the start of `strip` at Tuesday 12:00.

:::note
Without a per-link `LagUnit`, lag is measured in [`Config.DurationUnit`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#durationunit>).
:::

### Waiting for several predecessors

A task with several incoming links must satisfy all of them. In a forward project, the latest date any link requires wins:

```csharp
using Plen;

var monday = Moment.FromYmdHm(2026, 3, 2, 0, 0)!.Value;

var data = new ProjectData
{
    Tasks =
    [
        new TaskData("backend") { StartDate = monday, Duration = 2 },
        new TaskData("frontend") { StartDate = monday, Duration = 4 },
        new TaskData("release") { StartDate = monday, Duration = 1 },
    ],
    Links =
    [
        new LinkData { Id = 1, Source = "backend", Target = "release" },
        new LinkData { Id = 2, Source = "frontend", Target = "release" },
    ],
};

var result = Engine.Schedule(data, new SolveOptions { Reasons = true });
foreach (var update in result.Updates)
{
    Console.WriteLine($"{update.Id}: {update.StartDate} .. {update.EndDate} {update.Reason}");
}
```

Output:

```
release: 2026-03-06 00:00 .. 2026-03-07 00:00 Asap { LinkId = 2, SourceId = frontend }
```

`frontend` finishes last, so its link drives `release` to Friday. The [`Reason.Asap`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/reason.md>) names that driving link and predecessor.

### Links that get dropped

A link that can't apply is skipped and reported as a [`Note.DroppedLink`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/note.md>) 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 [`Config.DescendantLinks`](<https://docs.dhtmlx.com/planning-engine/net/api/core/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/net/scheduling/links-constraints-summaries.md#summary-tasks>))

```csharp
using Plen;

var monday = Moment.FromYmdHm(2026, 3, 2, 0, 0)!.Value;

var data = new ProjectData
{
    Tasks = [new TaskData("a") { StartDate = monday, Duration = 1 }],
    Links = [new LinkData { Id = "l1", Source = "a", Target = "ghost" }],
};

var result = Engine.Schedule(data, new SolveOptions());
foreach (var note in result.Notes)
{
    if (note is Note.DroppedLink dropped)
    {
        Console.WriteLine($"link {dropped.Link} dropped: {dropped.Reason}");
    }
}
```

Output:

```
link l1 dropped: unknown target task "ghost"
```

A [`Lag`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/link-data.md#lag>) that isn't a finite number is different: strict validation rejects the input with an [`InputException`](<https://docs.dhtmlx.com/planning-engine/net/api/core/errors/input-exception.md>) at `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/net/api/core/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 [`Config.ApplyConstraints`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#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

```csharp
using Plen;

var monday = Moment.FromYmdHm(2026, 3, 2, 0, 0)!.Value;
var thursday = Moment.FromYmdHm(2026, 3, 5, 0, 0)!.Value;

var data = new ProjectData
{
    Config = new SchedulingConfig { ApplyConstraints = true },
    Tasks =
    [
        new TaskData("install")
        {
            StartDate = monday,
            Duration = 2,
            ConstraintType = ConstraintType.Snet,
            ConstraintDate = thursday,
        },
    ],
};

var result = Engine.Schedule(data, new SolveOptions { Reasons = true });
foreach (var update in result.Updates)
{
    Console.WriteLine($"{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/net/api/core/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/net/api/core/results/conflict.md>):

```csharp
using Plen;

var monday = Moment.FromYmdHm(2026, 3, 2, 0, 0)!.Value;
var tuesday = Moment.FromYmdHm(2026, 3, 3, 0, 0)!.Value;

var data = new ProjectData
{
    Config = new SchedulingConfig { ApplyConstraints = true },
    Tasks =
    [
        new TaskData("build") { StartDate = monday, Duration = 3 },
        new TaskData("demo")
        {
            StartDate = tuesday,
            Duration = 1,
            ConstraintType = ConstraintType.Mso,
            ConstraintDate = tuesday,
        },
    ],
    Links = [new LinkData { Id = 1, Source = "build", Target = "demo" }],
};

var result = Engine.Schedule(data, new SolveOptions());
foreach (var conflict in result.Conflicts)
{
    if (conflict is Conflict.ConstraintViolation v)
    {
        Console.WriteLine($"{v.Task}: {v.Constraint} {v.Violation}, required {v.Required}, actual {v.Actual}");
    }
}
Console.WriteLine($"updates: {result.Updates.Count}");
```

Output:

```
demo: Mso LatestStart, required 2026-03-03 00:00, actual 2026-03-05 00:00
updates: 0
```

`build` finishes on Wednesday, so the link wants `demo` to start on Thursday. The `Mso` pin keeps it on Tuesday and nothing moves. The [`Violation`](<https://docs.dhtmlx.com/planning-engine/net/api/core/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:

```csharp
using Plen;

var monday = Moment.FromYmdHm(2026, 3, 2, 0, 0)!.Value;
var friday = Moment.FromYmdHm(2026, 3, 6, 0, 0)!.Value;

var data = new ProjectData
{
    Config = new SchedulingConfig { ApplyConstraints = true },
    Tasks =
    [
        new TaskData("order-food")
        {
            StartDate = monday,
            Duration = 1,
            ConstraintType = ConstraintType.Alap,
        },
        new TaskData("party")
        {
            StartDate = friday,
            Duration = 1,
            ConstraintType = ConstraintType.Mso,
            ConstraintDate = friday,
        },
    ],
    Links = [new LinkData { Id = 1, Source = "order-food", Target = "party" }],
};

var result = Engine.Schedule(data, new SolveOptions());
foreach (var update in result.Updates)
{
    Console.WriteLine($"{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 [`Config.ScheduleFromEnd`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#schedulefromend>) and [`Config.ProjectEnd`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#projectend>) instead. See [`SchedulingConfig`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md>).

### Inheriting a summary's constraint

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

```csharp
using Plen;

var monday = Moment.FromYmdHm(2026, 3, 2, 0, 0)!.Value;
var wednesday = Moment.FromYmdHm(2026, 3, 4, 0, 0)!.Value;

var data = new ProjectData
{
    Config = new SchedulingConfig { ApplyConstraints = true, ProjectConstraint = true },
    Tasks =
    [
        new TaskData("phase")
        {
            TaskType = TaskType.Summary,
            ConstraintType = ConstraintType.Snet,
            ConstraintDate = wednesday,
        },
        new TaskData("step") { Parent = "phase", StartDate = monday, Duration = 1 },
    ],
};

var result = Engine.Schedule(data, new SolveOptions());
foreach (var update in result.Updates)
{
    Console.WriteLine($"{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/net/api/core/results/mutation.md#mutationkind>) update. With `ProjectConstraint = false`, nothing in this example moves. [`Config.GroupMode`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#groupmode>) turns inheritance off again. That's the only thing `GroupMode` does.

## Gap behaviour

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

- [`GapBehavior.Preserve`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#gapbehavior-1>) (default) keeps each task at its preferred dates as long as its links allow them. A link only pushes a task 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:

```csharp
using Plen;

foreach (var gap in new[] { GapBehavior.Preserve, GapBehavior.Compress })
{
    Plan(gap);
}

static void Plan(GapBehavior gapBehavior)
{
    var monday = Moment.FromYmdHm(2026, 3, 2, 0, 0)!.Value;
    var thursday = Moment.FromYmdHm(2026, 3, 5, 0, 0)!.Value;

    var data = new ProjectData
    {
        Config = new SchedulingConfig { GapBehavior = gapBehavior },
        Tasks =
        [
            new TaskData("design") { StartDate = monday, Duration = 1 },
            new TaskData("review") { StartDate = thursday, Duration = 1 },
        ],
        Links = [new LinkData { Id = 1, Source = "design", Target = "review" }],
    };

    var result = Engine.Schedule(data, new SolveOptions { IncludeUnchanged = true });
    foreach (var update in result.Updates)
    {
        Console.WriteLine($"{gapBehavior} {update.Id}: starts {update.StartDate}");
    }
}
```

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/net/api/core/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/net/api/core/data/task-data.md#tasktype>), or `"project"` in JSON) groups child tasks through their `Parent` property. Its span is always rolled up from its children, and changed summaries appear in [`Updates`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#updates>) with `Kind == MutationKind.Summary`. Write them back to your model like any other update.

[`Config.MoveProjects`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#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 [`Config.UseProgress`](<https://docs.dhtmlx.com/planning-engine/net/api/core/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/net/api/core/results/note.md#droppedlink>).

With [`Config.StrictCalendar`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#strictcalendar>) on, a block that lands on non-working time is reported as a [`Conflict.CalendarNonWorking`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/conflict.md>) with the proposed and snapped dates.

### Moving a summary as a block

```csharp
using Plen;

Plan(moveProjects: true);
Plan(moveProjects: false);

static void Plan(bool moveProjects)
{
    var monday = Moment.FromYmdHm(2026, 3, 2, 0, 0)!.Value;
    var tuesday = Moment.FromYmdHm(2026, 3, 3, 0, 0)!.Value;

    var data = new ProjectData
    {
        Config = new SchedulingConfig { MoveProjects = moveProjects },
        Tasks =
        [
            new TaskData("prep") { StartDate = monday, Duration = 3 },
            new TaskData("phase") { TaskType = TaskType.Summary },
            new TaskData("a") { Parent = "phase", StartDate = monday, Duration = 1 },
            new TaskData("b") { Parent = "phase", StartDate = tuesday, Duration = 1 },
        ],
        Links = [new LinkData { Id = 1, Source = "prep", Target = "phase" }],
    };

    var result = Engine.Schedule(data, new SolveOptions { Reasons = true });
    Console.WriteLine($"MoveProjects = {moveProjects}");
    foreach (var update in result.Updates)
    {
        Console.WriteLine($"  {update.Id} {update.Kind}: {update.StartDate} .. {update.EndDate} {update.Reason}");
    }
}
```

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 the link lands on each leaf. Both start on Thursday with [`Reason.Asap`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/reason.md>), and `phase` shrinks to one day.

### Keeping completed work in place

With [`Config.UseProgress`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#useprogress>), a child with `Progress = 1` is completed and doesn't move with its block:

```csharp
using Plen;

var monday = Moment.FromYmdHm(2026, 3, 2, 0, 0)!.Value;
var tuesday = Moment.FromYmdHm(2026, 3, 3, 0, 0)!.Value;

var data = new ProjectData
{
    Config = new SchedulingConfig { UseProgress = true },
    Tasks =
    [
        new TaskData("prep") { StartDate = monday, Duration = 3 },
        new TaskData("phase") { TaskType = TaskType.Summary },
        new TaskData("done") { Parent = "phase", StartDate = monday, Duration = 1, Progress = 1 },
        new TaskData("open") { Parent = "phase", StartDate = tuesday, Duration = 1 },
    ],
    Links = [new LinkData { Id = 1, Source = "prep", Target = "phase" }],
};

var result = Engine.Schedule(data, new SolveOptions());
foreach (var update in result.Updates)
{
    Console.WriteLine($"{update.Id}: {update.StartDate} .. {update.EndDate}");
}
```

Output:

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

Only `open` moves. `done` stays on Monday, and the summary stretches to cover both. Its start didn't change, so the `phase` update carries only the new `EndDate`.

A completed child stays fixed, because "completed work doesn't move" is what `UseProgress` promises.

### Linking a summary to its own children

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

```csharp
using Plen;

var monday = Moment.FromYmdHm(2026, 3, 2, 0, 0)!.Value;

var data = new ProjectData
{
    Tasks =
    [
        new TaskData("phase") { TaskType = TaskType.Summary },
        new TaskData("kickoff") { Parent = "phase", StartDate = monday, Duration = 1 },
    ],
    Links = [new LinkData { Id = "l1", Source = "phase", Target = "kickoff", LinkType = LinkType.Ss }],
};

var result = Engine.Schedule(data, new SolveOptions());
foreach (var note in result.Notes)
{
    if (note is Note.DroppedLink dropped)
    {
        Console.WriteLine($"link {dropped.Link} dropped: {dropped.Reason}");
    }
}
```

Output:

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

Set [`Config.DescendantLinks = true`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md#descendantlinks>) 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/net/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`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#updates>)
- lists each unique simple cycle in [`result.Cycles`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#cycles>) as a [`Cycle`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/schedule-result.md#cycle>) with its `Tasks` and `Links`
- returns analytics with [`AnalyticsStatus.Invalid`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/analytics.md#analyticsstatus>) if you asked for any (see [`Analytics`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/analytics.md>))
- still installs a baseline on a [`Project`](<https://docs.dhtmlx.com/planning-engine/net/api/core/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 fails validation.

### Reporting cycles

```csharp
using Plen;

var monday = Moment.FromYmdHm(2026, 3, 2, 0, 0)!.Value;

var data = new ProjectData
{
    Tasks =
    [
        new TaskData("a") { StartDate = monday, Duration = 1 },
        new TaskData("b") { StartDate = monday, Duration = 1 },
        new TaskData("c") { StartDate = monday, Duration = 1 },
    ],
    Links =
    [
        new LinkData { Id = "ab", Source = "a", Target = "b" },
        new LinkData { Id = "ba", Source = "b", Target = "a" },
        new LinkData { Id = "bc", Source = "b", Target = "c" },
    ],
};

var project = new Project();
var options = new SolveOptions { Analytics = new AnalyticsOptions { CriticalPath = true } };
var result = project.Solve(data, options);

Console.WriteLine($"updates: {result.Updates.Count}");
foreach (var cycle in result.Cycles)
{
    Console.WriteLine($"cycle: tasks [{string.Join(", ", cycle.Tasks)}], links [{string.Join(", ", cycle.Links)}]");
}
Console.WriteLine($"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.

### Spotting a self-link

A link whose `Source` and `Target` are the same task shows up as a cycle with one task:

```csharp
using Plen;

var monday = Moment.FromYmdHm(2026, 3, 2, 0, 0)!.Value;

var data = new ProjectData
{
    Tasks = [new TaskData("a") { StartDate = monday, Duration = 1 }],
    Links = [new LinkData { Id = "self", Source = "a", Target = "a" }],
};

var result = Engine.Schedule(data, new SolveOptions());
foreach (var cycle in result.Cycles)
{
    Console.WriteLine($"cycle: tasks [{string.Join(", ", cycle.Tasks)}], links [{string.Join(", ", cycle.Links)}]");
}
```

Output:

```
cycle: tasks [a], links [self]
```

### Breaking a cycle

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

```csharp
using Plen;

var monday = Moment.FromYmdHm(2026, 3, 2, 0, 0)!.Value;

var data = new ProjectData
{
    Tasks =
    [
        new TaskData("a") { StartDate = monday, Duration = 1 },
        new TaskData("b") { StartDate = monday, Duration = 1 },
    ],
    Links =
    [
        new LinkData { Id = "ab", Source = "a", Target = "b" },
        new LinkData { Id = "ba", Source = "b", Target = "a" },
    ],
};

var project = new Project();
var result = project.Solve(data, new SolveOptions());

if (result.Cycles.Count > 0)
{
    var culprit = result.Cycles[0].Links[^1];
    var changes = new SchedulingChanges();
    changes.Links.Remove.Add(culprit);
    project.Apply(changes);

    result = project.Solve(null, new SolveOptions());
    Console.WriteLine($"cycles: {result.Cycles.Count}");
    foreach (var update in result.Updates)
    {
        Console.WriteLine($"{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/net/live-projects/editing-a-project.md>).

## See also

- [`LinkData`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/link-data.md>), with `LinkType`
- [`TaskData`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/task-data.md>), with `ConstraintType` and `TaskType`
- [`Conflict`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/conflict.md>) and [`Note`](<https://docs.dhtmlx.com/planning-engine/net/api/core/results/note.md>)
- [`SchedulingConfig`](<https://docs.dhtmlx.com/planning-engine/net/api/core/data/scheduling-config.md>) for `ApplyConstraints`, `GapBehavior`, `MoveProjects`, `ProjectConstraint`, `DescendantLinks`, `UseProgress` and `StrictCalendar`
- [Critical path and slack](<https://docs.dhtmlx.com/planning-engine/net/scheduling/critical-path-and-slack.md>)
