[Canonical page](<https://docs.dhtmlx.com/planning-engine/wasm/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, from a preview, or from dates you already have, without scheduling.

## How analytics work

Analytics are off by default. Ask for them with an [`analytics`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/common.md#analyticsoptions>) object in the call options. It has two flags, `criticalPath` and `slack`, and both default to `false`. The same object fits three calls:

- [`SolveOptions`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/solve-options.md>) for [`schedule`](<https://docs.dhtmlx.com/planning-engine/wasm/api/schedule.md>) and [`JsonProject#solve`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>). The analytics describe the freshly scheduled dates.
- [`PreviewOptions`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-preview.md#previewoptions>) for [`JsonProject#preview`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-preview.md>). The analytics describe the previewed dates.
- [`AnalyzeOptions`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-analyze.md#analyzeoptions>) for [`JsonProject#analyze`](<https://docs.dhtmlx.com/planning-engine/wasm/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`, the result's `analytics` field holds an object with `status` and the lists you asked for. With both flags off, `result.analytics` is left out of the JSON. Its shape is on [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/schedule-result.md#analytics>).

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. The gap between the two plans is the slack:

- **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 it's 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. `criticalPath` lists the critical task and link ids. `slack` holds one `{task, total, free}` entry per task.

A few rules shape the output:

- Slack is a number in the configured `duration_unit` (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, and it's critical when any of its non-summary descendants is.
- With `use_progress`, 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 `"invalid"`, and both lists are left out.

## Requesting analytics

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

```js
const { schedule } = require("./pkg-node/plen_wasm.js");

const data = {
  tasks: [
    { id: "design", start_date: "2026-08-10 08:00", duration: 2 },
    { id: "build", start_date: "2026-08-10 08:00", duration: 3 },
    { id: "docs", start_date: "2026-08-10 08:00", duration: 1 },
  ],
  links: [
    { id: "l1", source: "design", target: "build", type: "fs" },
    { id: "l2", source: "design", target: "docs", type: "fs" },
  ],
};
const options = { analytics: { criticalPath: true, slack: true } };

const response = JSON.parse(schedule(JSON.stringify(data), JSON.stringify(options)));
if (!response.ok) {
  const { kind, message, path } = response.error;
  throw new Error(`${kind}: ${message} (at ${path})`);
}

const { analytics } = response.result;
console.log("critical tasks:", analytics.criticalPath.tasks.join(", "));
console.log("critical links:", analytics.criticalPath.links.join(", "));
for (const { task, total, free } of analytics.slack) {
  console.log(`${task}: total ${total} free ${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`. The `analytics` part of the result looks like this:

```json
{
  "criticalPath": { "links": ["l1"], "tasks": ["design", "build"] },
  "slack": [
    { "free": 0, "task": "design", "total": 0 },
    { "free": 0, "task": "build", "total": 0 },
    { "free": 2, "task": "docs", "total": 2 }
  ],
  "status": "available"
}
```

`docs` can slip two working days before the project end moves. It ends on Thursday and `build` ends the following Monday, and the weekend doesn't count.

[`JsonProject#solve`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>) takes the same options. Its cache key is the whole options object, so `solve(undefined, …)` with analytics turned on recomputes from the accepted dates instead of returning a cached result without them.

### Checking a change before you commit it

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

```js
const { JsonProject } = require("./pkg-node/plen_wasm.js");

function unwrap(text) {
  const response = JSON.parse(text);
  if (!response.ok) {
    const { kind, code, message, path } = response.error;
    throw new Error(`${kind} ${code ?? ""}: ${message} (at ${path})`);
  }
  return response.result;
}

const data = {
  tasks: [
    { id: "design", start_date: "2026-08-10 08:00", duration: 2 },
    { id: "build", start_date: "2026-08-10 08:00", duration: 3 },
    { id: "docs", start_date: "2026-08-10 08:00", duration: 1 },
  ],
  links: [
    { id: "l1", source: "design", target: "build", type: "fs" },
    { id: "l2", source: "design", target: "docs", type: "fs" },
  ],
};

const project = new JsonProject("{}");
try {
  unwrap(project.solve(JSON.stringify(data), "{}"));

  // What if the docs take five days instead of one?
  const changes = { tasks: { update: [{ id: "docs", duration: 5 }] } };
  const options = { analytics: { criticalPath: true, slack: true } };
  const result = unwrap(project.preview(JSON.stringify(changes), JSON.stringify(options)));

  console.log("critical:", result.analytics.criticalPath.tasks.join(", "));
  for (const { task, total, free } of result.analytics.slack) {
    console.log(`${task}: total ${total} free ${free}`);
  }
} finally {
  project.free();
}
```

With five days of docs, the critical path moves to `design, docs`, and `build` now has two days of slack:

```
critical: design, docs
design: total 0 free 0
build: total 2 free 2
docs: total 0 free 0
```

The project itself doesn't change. For more on previewing, see [Previewing changes](<https://docs.dhtmlx.com/planning-engine/wasm/live-projects/previewing-changes.md>).

## Analyzing without scheduling

Use [`analyze`](<https://docs.dhtmlx.com/planning-engine/wasm/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.

```js
const { JsonProject } = require("./pkg-node/plen_wasm.js");

function unwrap(text) {
  const response = JSON.parse(text);
  if (!response.ok) {
    const { kind, code, message, path } = response.error;
    throw new Error(`${kind} ${code ?? ""}: ${message} (at ${path})`);
  }
  return response.result;
}

// "build" starts a day after "design" ends. A solve would pull it back to Wednesday.
const data = {
  tasks: [
    { id: "design", start_date: "2026-08-10 08:00", duration: 2 },
    { id: "build", start_date: "2026-08-13 08:00", duration: 3 },
  ],
  links: [{ id: "l1", source: "design", target: "build", type: "fs" }],
};
const options = { analytics: { criticalPath: true, slack: true } };

const project = new JsonProject("{}");
try {
  const result = unwrap(project.analyze(JSON.stringify(data), JSON.stringify(options)));
  console.log(result.updates.length); // 0
  console.log(JSON.stringify(result.analytics));
} finally {
  project.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.

```json
{
  "criticalPath": { "links": [], "tasks": ["build"] },
  "slack": [
    { "free": 1, "task": "design", "total": 1 },
    { "free": 0, "task": "build", "total": 0 }
  ],
  "status": "available"
}
```

`analyze` is read-only. With data, it validates and reads the snapshot but doesn't install it, so the project keeps whatever it held before. A fresh `new JsonProject("{}")` is enough when all you need is the analysis. Invalid data or options come back as an error envelope with kind `parse` or `validation`, as in [Error handling](<https://docs.dhtmlx.com/planning-engine/wasm/error-handling.md>). The result has an empty `updates` list and no planner conflicts, and it has no [`baselineId`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/schedule-result.md#baselineid>).

### Analyzing the project's current dates

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

```js
const { JsonProject } = require("./pkg-node/plen_wasm.js");

function unwrap(text) {
  const response = JSON.parse(text);
  if (!response.ok) {
    const { kind, code, message, path } = response.error;
    throw new Error(`${kind} ${code ?? ""}: ${message} (at ${path})`);
  }
  return response.result;
}

const data = {
  tasks: [
    { id: "design", start_date: "2026-08-10 08:00", duration: 2 },
    { id: "build", start_date: "2026-08-10 08:00", duration: 3 },
    { id: "docs", start_date: "2026-08-10 08:00", duration: 1 },
  ],
  links: [
    { id: "l1", source: "design", target: "build", type: "fs" },
    { id: "l2", source: "design", target: "docs", type: "fs" },
  ],
};
const options = JSON.stringify({ analytics: { criticalPath: true } });

const project = new JsonProject("{}");
try {
  unwrap(project.solve(JSON.stringify(data), "{}"));

  // Solved and clean: the result names the baseline it read.
  const clean = unwrap(project.analyze(undefined, options));
  console.log(clean.baselineId !== undefined); // true

  // Commit an edit without solving.
  const report = unwrap(project.apply(JSON.stringify({ tasks: { update: [{ id: "docs", duration: 5 }] } })));
  if (report.needsResync) throw new Error("edit rejected; resync with solve(dataJson, ...)");

  const dirty = unwrap(project.analyze(undefined, options));
  console.log(dirty.baselineId !== undefined); // false
  console.log("critical:", dirty.analytics.criticalPath.tasks.join(", ")); // design, docs
} finally {
  project.free();
}
```

After the edit, the critical path is `design, docs`. `analyze` doesn't change the project's solved status, so the project still needs [`solve(undefined, …)`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>) before [`preview`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-preview.md>) works again. Until then, `preview` returns a `project` error with code [`no-baseline`](<https://docs.dhtmlx.com/planning-engine/wasm/error-handling.md#project-errors>).

`baselineId` is set only when the project is solved and has no unsolved edits. That's how you tell whether the analytics describe the accepted baseline. When there's no baseline, the field is left out, never `null`.

## Reading the result

Check `status` first. Then read the lists you asked for: a flag you left off means its list is missing from the JSON, even when the status is `"available"`.

```js
const { schedule } = require("./pkg-node/plen_wasm.js");

const data = {
  config: { use_progress: true },
  tasks: [
    { id: "phase", type: "summary" },
    { id: "spec", parent: "phase", start_date: "2026-08-10 08:00", duration: 1, progress: 1 },
    { id: "review", parent: "phase", start_date: "2026-08-10 08:00", duration: 2 },
    { id: "release", start_date: "2026-08-10 08:00", duration: 1 },
  ],
  links: [{ id: "l1", source: "review", target: "release", type: "fs" }],
};
const options = { analytics: { criticalPath: true, slack: true } };

const response = JSON.parse(schedule(JSON.stringify(data), JSON.stringify(options)));
if (!response.ok) {
  const { kind, message, path } = response.error;
  throw new Error(`${kind}: ${message} (at ${path})`);
}

const { analytics, cycles } = response.result;
if (analytics.status === "invalid") {
  console.log(`${cycles.length} dependency cycle(s), no analytics`);
} else {
  console.log(`critical: ${analytics.criticalPath.tasks.join(", ")}`);
  for (const { task, total, free } of analytics.slack) {
    console.log(`${task}: total ${total} free ${free}`);
  }
}
```

The output shows each rule from the concept section:

```
critical: review, phase, release
spec: total 0 free 0
review: total 0 free 0
release: total 0 free 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.

Ids come back as you sent them: a numeric id stays a JSON number and a string id stays a string. Slack values are plain numbers in `duration_unit`. With `"duration_unit": "hour"`, a task that can slip one and a half working days reports `total: 12`.

If the links form a cycle, the solve returns unchanged dates and lists each cycle in [`result.cycles`](<https://docs.dhtmlx.com/planning-engine/wasm/api/types/schedule-result.md#cycle>). The analytics then come back with only a status:

```json
{
  "analytics": { "status": "invalid" },
  "cycles": [{ "links": [1, 2], "tasks": [1, 2] }]
}
```

Fix the cycle and run again. For the rest of the result, see [Reading results](<https://docs.dhtmlx.com/planning-engine/wasm/scheduling/reading-results.md>).
