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


# Critical path and slack

This guide shows how to find the tasks that decide the project's finish date and how much room every other task has. You can get these numbers from a scheduling run, or from dates you already have without scheduling anything.

## How analytics work

Analytics are off by default. You ask for them with an [`AnalyticsOptions`](<https://docs.dhtmlx.com/planning-engine/java/api/project/analytics-options.md>) object, which has two flags: `criticalPath` and `slack`. The same object fits into three option types:

- [`SolveOptions.analytics`](<https://docs.dhtmlx.com/planning-engine/java/api/project/solve-options.md#analytics>), for [`Engine.schedule`](<https://docs.dhtmlx.com/planning-engine/java/api/engine-schedule.md>) and [`Project#solve`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>). The analytics describe the freshly scheduled dates.
- [`PreviewOptions.analytics`](<https://docs.dhtmlx.com/planning-engine/java/api/project/preview-options.md#analytics>), for [`Project#preview`](<https://docs.dhtmlx.com/planning-engine/java/api/project-preview.md>). The analytics describe the previewed dates.
- [`AnalyzeOptions.analytics`](<https://docs.dhtmlx.com/planning-engine/java/api/project/analyze-options.md#analytics>), for [`Project#analyze`](<https://docs.dhtmlx.com/planning-engine/java/api/project-analyze.md>). The analytics describe dates you supply, or the project's current dates, as they are. Nothing gets scheduled.

When at least one flag is `true`, [`ScheduleResult.analytics`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#analytics>) holds an [`Analytics`](<https://docs.dhtmlx.com/planning-engine/java/api/results/analytics.md>) object. With both flags off, the field is `null`.

The engine computes the numbers from two plans. The first plan is the dates being analyzed. For the second, the engine schedules the same tasks backward from the latest finish, which gives each task its latest possible start. Slack is the gap between the two:

- **Total slack** is how far a task can slip before the project's finish date moves. It's the latest start minus the planned start.
- **Free slack** is how far a task can slip before it pushes any successor. It's the smallest margin over the task's outgoing links and never more than the total slack. A task with no successors has free slack equal to its total slack.

A task is **critical** when its total slack is zero or less. A link is critical when both of its tasks are critical and the link is binding, so its target starts exactly when the link allows. `Analytics.criticalPath` is a [`Critical`](<https://docs.dhtmlx.com/planning-engine/java/api/results/analytics.md#critical>) object with the critical task ids in `tasks` and link ids in `links`. `Analytics.slack` is a [`Vec<TaskSlack>`](<https://docs.dhtmlx.com/planning-engine/java/api/results/analytics.md#taskslack>) with one entry per task.

A few rules shape the output:

- Slack is a `double` in `config.durationUnit` (days by default) and counts working time on each task's calendar. A weekend between two tasks doesn't add slack.
- Only leaf tasks and milestones get a slack entry. A summary has no slack. It's critical when any of its non-summary descendants is.
- With `useProgress`, completed tasks (`progress == 1`) report zero total and free slack, but they're never critical.
- A dependency cycle makes the numbers meaningless. `status` is then [`AnalyticsStatus.INVALID`](<https://docs.dhtmlx.com/planning-engine/java/api/results/analytics.md#analyticsstatus>), and both `criticalPath` and `slack` are `null`.

## Getting critical path and slack

### Requesting analytics

Set the flags in [`SolveOptions`](<https://docs.dhtmlx.com/planning-engine/java/api/project/solve-options.md>) to get analytics together with the schedule.

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

public class SolveAnalytics {
  static TaskData task(String id, Moment start, double duration) {
    return new TaskData(Id.str(id)).withStartDate(start).withDuration(duration);
  }

  static LinkData fs(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, 8, 10, 8, 0);
    ProjectData data = new ProjectData()
        .withTasks(List.of(
            task("design", monday, 2.0),
            task("build", monday, 3.0),
            task("docs", monday, 1.0)))
        .withLinks(List.of(fs("l1", "design", "build"), fs("l2", "design", "docs")));

    SolveOptions options = new SolveOptions()
        .withAnalytics(new AnalyticsOptions().withCriticalPath(true).withSlack(true));
    ScheduleResult result = Engine.schedule(data, options);

    Analytics analytics = result.analytics;
    for (Id id : analytics.criticalPath.tasks) {
      System.out.println("critical task: " + id);
    }
    for (Id id : analytics.criticalPath.links) {
      System.out.println("critical link: " + id);
    }
    for (TaskSlack s : analytics.slack) {
      System.out.println(s.task + ": total " + s.total + " free " + s.free);
    }
  }
}
```

Both `build` and `docs` wait for `design`. `build` takes three days and `docs` takes one, so `design` and `build` form the critical path, joined by link `l1`. `docs` reports `total 2.0 free 2.0`: it can slip two working days before the project end moves.

[`Project#solve`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) takes the same `SolveOptions`. The solve cache key is the whole `SolveOptions` object, so `solve(null, options)` with analytics turned on recomputes from the accepted dates. You don't get back a cached result without them.

### Checking a change before you commit it

Set the flags in [`PreviewOptions`](<https://docs.dhtmlx.com/planning-engine/java/api/project/preview-options.md>) to see how an edit would change the critical path.

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

public class PreviewAnalytics {
  static TaskData task(String id, Moment start, double duration) {
    return new TaskData(Id.str(id)).withStartDate(start).withDuration(duration);
  }

  static LinkData fs(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, 8, 10, 8, 0);
    ProjectData data = new ProjectData()
        .withTasks(List.of(
            task("design", monday, 2.0),
            task("build", monday, 3.0),
            task("docs", monday, 1.0)))
        .withLinks(List.of(fs("l1", "design", "build"), fs("l2", "design", "docs")));

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

    // What if the docs take five days instead of one?
    SchedulingChanges changes = new SchedulingChanges();
    changes.tasks.update.add(new TaskUpdate(Id.str("docs")).withDuration(Patch.of(5.0)));

    PreviewOptions options = new PreviewOptions()
        .withAnalytics(new AnalyticsOptions().withCriticalPath(true).withSlack(true));
    ScheduleResult result = project.preview(changes, options);

    Analytics analytics = result.analytics;
    System.out.println("critical: " + analytics.criticalPath.tasks.stream()
        .map(Id::toString)
        .collect(Collectors.joining(", ")));
    for (TaskSlack s : analytics.slack) {
      System.out.println(s.task + ": total " + s.total + " free " + s.free);
    }
  }
}
```

With five days of docs, the critical path moves to `design, docs`, and `build` now has two days of slack. The project itself doesn't change. For more on previewing, see [Previewing changes](<https://docs.dhtmlx.com/planning-engine/java/live-projects/previewing-changes.md>).

### Analyzing without scheduling

Use [`Project#analyze`](<https://docs.dhtmlx.com/planning-engine/java/api/project-analyze.md>) when you already have dates, for example from your own database or a gantt chart, and want the critical path for those exact dates.

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

public class AnalyzeDates {
  static TaskData task(String id, Moment start, double duration) {
    return new TaskData(Id.str(id)).withStartDate(start).withDuration(duration);
  }

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

    // "build" starts a day after "design" ends. A solve would pull it back to Wednesday.
    ProjectData data = new ProjectData()
        .withTasks(List.of(task("design", monday, 2.0), task("build", thursday, 3.0)))
        .withLinks(List.of(new LinkData()
            .withId(Id.str("l1"))
            .withSource(Id.str("design"))
            .withTarget(Id.str("build"))));

    Project project = new Project();
    AnalyzeOptions options = new AnalyzeOptions()
        .withAnalytics(new AnalyticsOptions().withCriticalPath(true).withSlack(true));
    ScheduleResult result = project.analyze(data, options);
    System.out.println("updates: " + result.updates.size());

    Analytics analytics = result.analytics;
    for (Id id : analytics.criticalPath.tasks) {
      System.out.println("critical task: " + id);
    }
    for (TaskSlack s : analytics.slack) {
      System.out.println(s.task + ": total " + s.total + " free " + s.free);
    }
  }
}
```

Only `build` is critical. `design` has one day of total and free slack, because `build` starts a day later than the link requires. Link `l1` isn't critical: it isn't binding, and `design` isn't critical.

`analyze` never changes the project. With non-null `data` it validates and reads the snapshot but doesn't install it. Invalid data throws [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>), as described in [Error handling](<https://docs.dhtmlx.com/planning-engine/java/error-handling.md>). The result's `updates` list is empty, `conflicts` is empty, and `baselineId` is `null`.

### Analyzing the project's current dates

Pass `null` as the data to analyze the dates the project holds now, including edits that haven't been solved yet.

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

public class AnalyzeCurrent {
  static TaskData task(String id, Moment start, double duration) {
    return new TaskData(Id.str(id)).withStartDate(start).withDuration(duration);
  }

  static LinkData fs(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, 8, 10, 8, 0);
    ProjectData data = new ProjectData()
        .withTasks(List.of(
            task("design", monday, 2.0),
            task("build", monday, 3.0),
            task("docs", monday, 1.0)))
        .withLinks(List.of(fs("l1", "design", "build"), fs("l2", "design", "docs")));

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

    AnalyzeOptions options = new AnalyzeOptions()
        .withAnalytics(new AnalyticsOptions().withCriticalPath(true));

    // Solved and clean: the result names the baseline it read.
    ScheduleResult clean = project.analyze(null, options);
    System.out.println("clean baseline: " + (clean.baselineId != null));

    // Commit an edit without solving.
    SchedulingChanges changes = new SchedulingChanges();
    changes.tasks.update.add(new TaskUpdate(Id.str("docs")).withDuration(Patch.of(5.0)));
    project.apply(changes);

    ScheduleResult dirty = project.analyze(null, options);
    System.out.println("dirty baseline: " + (dirty.baselineId != null));
    System.out.println("critical: " + dirty.analytics.criticalPath.tasks.stream()
        .map(Id::toString)
        .collect(Collectors.joining(", ")));
  }
}
```

After the edit, the critical path is `design, docs`. `analyze` doesn't change the project's solved status, so the project still needs `solve(null, ...)` before `preview` works again. `baselineId` is set only when the project is solved and has no unsolved edits. Check it to tell whether the analytics describe the accepted baseline.

### Reading the result

Check `status` first. Then read the lists you asked for. A flag you left off gives `null` for its list, even when the status is `AVAILABLE`. In the example above, `slack` was never requested, so `dirty.analytics.slack` is `null`.

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

public class ReadResult {
  static TaskData task(String id, Moment start, double duration) {
    return new TaskData(Id.str(id)).withStartDate(start).withDuration(duration);
  }

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

    ProjectData data = new ProjectData()
        .withConfig(new SchedulingConfig().withUseProgress(true))
        .withTasks(List.of(
            new TaskData(Id.str("phase")).withTaskType(TaskType.SUMMARY),
            task("spec", monday, 1.0).withParent(Id.str("phase")).withProgress(1.0),
            task("review", monday, 2.0).withParent(Id.str("phase")),
            task("release", monday, 1.0)))
        .withLinks(List.of(new LinkData()
            .withId(Id.str("l1"))
            .withSource(Id.str("review"))
            .withTarget(Id.str("release"))));

    SolveOptions options = new SolveOptions()
        .withAnalytics(new AnalyticsOptions().withCriticalPath(true).withSlack(true));
    ScheduleResult result = Engine.schedule(data, options);

    Analytics analytics = result.analytics;
    if (analytics.status == AnalyticsStatus.INVALID) {
      System.out.println(result.cycles.size() + " dependency cycle(s), no analytics");
      return;
    }
    System.out.println("critical: " + analytics.criticalPath.tasks.stream()
        .map(Id::toString)
        .collect(Collectors.joining(", ")));
    for (TaskSlack s : analytics.slack) {
      System.out.println(s.task + ": total " + s.total + " free " + s.free);
    }
  }
}
```

The output shows each rule from the concept section:

```text
critical: review, phase, release
spec: total 0.0 free 0.0
review: total 0.0 free 0.0
release: total 0.0 free 0.0
```

- `phase` is critical because its child `review` is. It has no slack entry, since summaries never get one.
- `spec` is complete, so it reports zero slack. It still isn't on the critical path.
- The critical task list follows the scheduling graph order, not input order. Look tasks up by id instead of relying on their position.

If the links form a cycle, the solve returns unchanged dates and lists each cycle in `result.cycles`. The analytics then come back as `INVALID`, with `criticalPath` and `slack` both `null`. Fix the cycle and run again. For the rest of the result, see [Reading results](<https://docs.dhtmlx.com/planning-engine/java/scheduling/reading-results.md>).
