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


# Reading results

Every scheduling call returns a [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/js/api/types/schedule-result.md>). This guide shows how to write the new dates back into your own model and explain why tasks moved. It also covers constraint conflicts, dependency cycles and dropped records, the baseline token, and the form dates take in results.

## How a result is organized

A `ScheduleResult` is what [`schedule`](<https://docs.dhtmlx.com/planning-engine/js/api/schedule.md>), [`project.solve`](<https://docs.dhtmlx.com/planning-engine/js/api/project-solve.md>), [`project.preview`](<https://docs.dhtmlx.com/planning-engine/js/api/project-preview.md>) and [`project.analyze`](<https://docs.dhtmlx.com/planning-engine/js/api/project-analyze.md>) return. [`project.level`](<https://docs.dhtmlx.com/planning-engine/js/api/project-level.md>) returns the same object with an extra [`rounds`](<https://docs.dhtmlx.com/planning-engine/js/api/project-level.md#levelingresult>) field. Its fields fall into four groups:

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

A few rules hold for every result:

- **Updates are sparse.** `updates` lists only the tasks whose dates changed, and each entry carries only the fields that changed. A task that didn't move isn't there. Set [`includeUnchanged`](<https://docs.dhtmlx.com/planning-engine/js/api/types/solve-options.md#includeunchanged>) on `solve` when you need every task's dates, for example on the first load.
- **Problems are data, not errors.** A plan with conflicts, cycles or dropped links still returns a result. Only invalid input and calls that don't fit the project's state throw. See [Error handling](<https://docs.dhtmlx.com/planning-engine/js/error-handling.md>).
- **Order is stable.** The same input and options always produce the same result, in the same order. The one exception is `baselineId`, which is an opaque token.
- **Updates use the input names.** An update entry uses the same field names as [`TaskData`](<https://docs.dhtmlx.com/planning-engine/js/api/types/task-data.md>) and [`TaskUpdate`](<https://docs.dhtmlx.com/planning-engine/js/api/types/task-update.md>): `start_date`, `end_date`, `duration`, `leveling_delay`, `pauses`. You can pass an entry back as a `TaskUpdate`; its `kind` and `reason` fields are ignored there.
- **Results are yours.** Every call returns fresh plain objects that share nothing with your input or with the project. Changing a result can't affect later calls.

:::note
The leveling result is a `ScheduleResult` with an added `rounds` field; read `updates` on it.
:::

## Updates

Read [`updates`](<https://docs.dhtmlx.com/planning-engine/js/api/types/schedule-result.md#mutation>) after each call and copy the changed fields into your model. A missing field means "unchanged", not "cleared".

```js
import { schedule } from "plen-js";

// The caller's own copy of the tasks.
const model = new Map([
  [1, { id: 1, text: "Design", start_date: "2026-08-10 08:00", duration: 2 }],
  [2, { id: 2, text: "Build", start_date: "2026-08-10 08:00", duration: 1 }],
]);

const result = schedule({
  tasks: [...model.values()],
  links: [{ id: 1, source: 1, target: 2, type: "fs" }],
});

for (const update of result.updates) {
  console.log(JSON.stringify(update));
  const task = model.get(update.id);
  if (!task) continue;
  for (const field of ["start_date", "end_date", "duration", "leveling_delay", "pauses"]) {
    if (field in update) task[field] = update[field];
  }
}
```

Output:

```
{"end_date":"2026-08-13 08:00","id":2,"kind":"task","start_date":"2026-08-12 08:00"}
```

Task 1 is missing because it didn't move. Task 2 moved, so it reports a new start and end. It has no `duration`, because its working time didn't change. The extra `text` field in the model is ignored by the scheduler.

`kind` is `"task"`, `"summary"` or `"milestone"`. Summaries appear in `updates` too, because their spans roll up from their children. `leveling_delay` and `pauses` only appear in results from [`project.level`](<https://docs.dhtmlx.com/planning-engine/js/api/project-level.md>), and only when leveling changed them. [Leveling resources](<https://docs.dhtmlx.com/planning-engine/js/resources-and-assignment/leveling-resources.md>) covers how to write them back.

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

## Reasons

Turn on `reasons` in the [solve options](<https://docs.dhtmlx.com/planning-engine/js/api/types/solve-options.md>) to learn why each task ended up where it did. Each update then carries a [`reason`](<https://docs.dhtmlx.com/planning-engine/js/api/types/schedule-result.md#reason>) object, tagged by `kind`:

```js
import { schedule } from "plen-js";

const result = schedule(
  {
    tasks: [1, 2, 3].map((id) => ({ id, start_date: "2026-08-10 08:00", duration: 1 })),
    links: [
      { id: 10, source: 1, target: 2 },
      { id: 11, source: 2, target: 3 },
    ],
  },
  { reasons: true },
);

function explain(reason) {
  switch (reason?.kind) {
    case "asap":
      return `pushed by task ${reason.sourceId} through link ${reason.linkId}`;
    case "alap":
      return `pulled by task ${reason.targetId} through link ${reason.linkId}`;
    case "summary-translation":
      return `moved with summary ${reason.summaryId}`;
    case "constraint":
      return "placed by its constraint";
    case "preferred":
      return "kept at its preferred date";
    default:
      return "no reason recorded";
  }
}

for (const update of result.updates) {
  console.log(`task ${update.id}: ${explain(update.reason)}`);
}
```

Output:

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

The kinds:

- `"asap"` (`linkId`, `sourceId`): a predecessor link pushed the task later.
- `"alap"` (`linkId`, `targetId`): a successor link pulled the task earlier (backward planning).
- `"summary-translation"` (`summaryId`): the task moved as part of its summary block.
- `"constraint"`: the task's own constraint placed it.
- `"preferred"`: the task stayed at, or returned to, its own date.

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

## Conflicts

A conflict is a rule the scheduled plan breaks. It doesn't stop scheduling: the dates are still valid output, and [`conflicts`](<https://docs.dhtmlx.com/planning-engine/js/api/types/schedule-result.md#conflict>) lists what doesn't fit. Each conflict is tagged by `kind`:

- `"constraint-violation"` (`task`, `constraint`, `violation`, `required`, `actual`): a task's constraint and its links disagree. `violation` names the bound that broke: `"earliest-start"`, `"latest-start"`, `"earliest-end"` or `"latest-end"`.
- `"calendar-non-working"` (`task`, `proposed`, `snapped`): with `config.strict_calendar`, a task moved with its summary block landed on non-working time.

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

```js
import { schedule } from "plen-js";

const result = schedule({
  config: { apply_constraints: true },
  tasks: [
    { id: 1, start_date: "2026-08-10 08:00", duration: 2 },
    // Build must start no later than Tuesday, but design runs until Wednesday.
    {
      id: 2,
      start_date: "2026-08-10 08:00",
      duration: 1,
      constraint_type: "snlt",
      constraint_date: "2026-08-11 08:00",
    },
  ],
  links: [{ id: 1, source: 1, target: 2 }],
});

for (const conflict of result.conflicts) {
  if (conflict.kind === "constraint-violation") {
    console.log(
      `task ${conflict.task}: ${conflict.constraint} violated (${conflict.violation}), ` +
        `required ${conflict.required}, actual ${conflict.actual}`,
    );
  } else if (conflict.kind === "calendar-non-working") {
    console.log(`task ${conflict.task}: lands on non-working time at ${conflict.proposed} (snapped ${conflict.snapped})`);
  }
}
for (const update of result.updates) {
  console.log(`task ${update.id}: start ${update.start_date}`);
}
```

Output:

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

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

## Cycles and notes

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

[`notes`](<https://docs.dhtmlx.com/planning-engine/js/api/types/schedule-result.md#note>) lists records the scheduler left out or changed. Each note is tagged by `kind`:

- `"dropped-link"` (`link`, `reason`): a link that can't apply, for example one that points to an unscheduled task.
- `"dropped-task"` (`task`, `reason`): an unscheduled task.
- `"dropped-assignment"` (`assignment`, `reason`): an assignment that doesn't take part.
- `"repaired"` (`path`, `message`): a fix applied under `validation: "repair"`.

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

```js
import { schedule } from "plen-js";

const result = schedule({
  tasks: [
    { id: 1, start_date: "2026-08-10 08:00", duration: 1 },
    { id: 2, start_date: "2026-08-10 08:00", duration: 1 },
    // Task 3 is parked: it isn't scheduled, and neither is the link to it.
    { id: 3, start_date: "2026-08-10 08:00", duration: 1, unscheduled: true },
  ],
  links: [
    // 1 -> 2 -> 1 is a cycle.
    { id: 10, source: 1, target: 2 },
    { id: 11, source: 2, target: 1 },
    { id: 12, source: 2, target: 3 },
  ],
});

for (const cycle of result.cycles) {
  console.log(`cycle: tasks [${cycle.tasks.join(", ")}], links [${cycle.links.join(", ")}]`);
}
console.log(`updates: ${result.updates.length}`);

for (const note of result.notes) {
  switch (note.kind) {
    case "dropped-link":
      console.log(`link ${note.link} dropped: ${note.reason}`);
      break;
    case "dropped-task":
      console.log(`task ${note.task} dropped: ${note.reason}`);
      break;
    case "dropped-assignment":
      console.log(`assignment ${note.assignment} dropped: ${note.reason}`);
      break;
    case "repaired":
      console.log(`repaired ${note.path}: ${note.message}`);
      break;
  }
}
if (!result.converged) {
  console.log("the planner stopped before a fixed point");
}
```

Output:

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

Highlight the links in `cycle.links` so the user can break the loop. A cyclic solve still installs a baseline. If you requested analytics, they come back with `status: "invalid"` and no critical path or slack. See [Critical path and slack](<https://docs.dhtmlx.com/planning-engine/js/scheduling/critical-path-and-slack.md>).

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

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

## Baseline token

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

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

When there's no baseline to name, `baselineId` is left out of the result, not set to `null`.

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

```js
import { Project, ProjectError } from "plen-js";

const project = new Project();
const loaded = project.solve({
  tasks: [
    { id: 1, start_date: "2026-08-10 08:00", duration: 2 },
    { id: 2, start_date: "2026-08-10 08:00", duration: 1 },
  ],
  links: [{ id: 1, source: 1, target: 2 }],
});
// Keep the token next to the dates the UI shows.
const shown = loaded.baselineId;

const changes = { tasks: { update: [{ id: 1, duration: 3 }] } };
const preview = project.preview(changes, { baselineId: shown });
console.log("preview read the shown baseline:", preview.baselineId === shown);

// Someone else commits an edit; the accepted baseline changes.
project.apply(changes);
const committed = project.solve(null);
console.log("token changed:", committed.baselineId !== shown);

try {
  project.preview(changes, { baselineId: shown });
} catch (error) {
  if (error instanceof ProjectError && error.code === "stale-baseline") {
    console.log("stale: reload the dates before previewing again");
  } else {
    throw error;
  }
}
```

Output:

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

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

## Dates in results

By default, every date in a result is a `"YYYY-MM-DD HH:MM"` string: `start_date` and `end_date` in updates, `required` and `actual` in conflicts, `proposed` and `snapped`, overallocation window edges, load-profile buckets. Strings are naive local wall-clock time with no timezone, so you can show them as they are or send them to a server unchanged.

Create the [project](<https://docs.dhtmlx.com/planning-engine/js/api/project.md#projectoptions>) with `dates: "date"` to get local `Date` objects instead. The year, month, day, hour and minute of each `Date` match the string form in the timezone of the running code:

```js
import { Project } from "plen-js";

const project = new Project({ dates: "date" });
const result = project.solve({
  config: { apply_constraints: true },
  tasks: [
    { id: 1, start_date: "2026-08-10 08:00", duration: 2 },
    {
      id: 2,
      start_date: "2026-08-10 08:00",
      duration: 1,
      constraint_type: "snlt",
      constraint_date: "2026-08-11 08:00",
    },
  ],
  links: [{ id: 1, source: 1, target: 2 }],
});

const [update] = result.updates;
console.log(update.start_date instanceof Date); // true
console.log(update.start_date.getDate(), update.start_date.getHours()); // 11 8

const [conflict] = result.conflicts;
console.log(conflict.required instanceof Date); // true
```

The option only changes the output. Input accepts strings and `Date` objects either way, so you can pass a result's dates straight back into a change set.

:::note
Set `dates: "date"` to get result dates as local `Date` objects instead of `"YYYY-MM-DD HH:MM"` strings; `schedule()` always returns strings.
:::

Keep the default strings when you store results or send them over the network: a result with string dates is plain JSON and survives `JSON.stringify` as is. [Plain objects and dates](<https://docs.dhtmlx.com/planning-engine/js/plain-objects-and-dates.md>) covers the date formats and field spelling in detail.
