[Canonical page](<https://docs.dhtmlx.com/planning-engine/js/live-projects/previewing-changes/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Previewing Changes

This guide shows how to see what an edit would do before you commit it. It covers previewing a drag or a resize, keeping previews in sync with the project's baseline, checking the resource load a change would cause, and trying multi-step edits or a leveling run on a copy of the project.

## How previews work

A solved [`Project`](<https://docs.dhtmlx.com/planning-engine/js/api/project.md>) holds one **accepted baseline**: the dates from its 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>). Every preview starts from that baseline.

[`project.preview(changes, options)`](<https://docs.dhtmlx.com/planning-engine/js/api/project-preview.md>) copies the baseline, applies a [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/js/api/types/scheduling-changes.md>) object to the copy and schedules it. The copy is thrown away when the call returns. The project stays as it was, so you can call `preview` as often as the UI needs, for example on every mouse move during a drag. Like every `Project` method, `preview` is synchronous.

What you get back is a regular [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/js/api/types/schedule-result.md>):

- `updates` are relative to the baseline. They list the tasks the change would move and the tasks the change itself touched, the same way `solve(null)` reports them after an [`apply`](<https://docs.dhtmlx.com/planning-engine/js/api/project-apply.md>). An empty change set gives empty `updates`.
- `conflicts`, `cycles` and `notes` describe the previewed state. A preview can show the cycle a new link would create, and the project never holds that link.
- `baselineId` names the baseline the preview read.

A preview differs from `apply` in two ways. It isn't partial: if any operation in the change set is invalid, the whole call throws an [`InputError`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md#inputerror>). And it needs a clean, solved baseline. Before the first solve, and after any `apply` until the next `solve`, it throws a [`ProjectError`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md#projecterror>) with code `"no-baseline"`.

For a change that takes more than one step, or for a `level` run, use [`project.clone()`](<https://docs.dhtmlx.com/planning-engine/js/api/project.md#clone>) instead. The clone is an independent `Project` with the same baseline, cached result and options. Edit, solve or level the clone, and the original stays as it was.

| You want to see | Use |
|---|---|
| The effect of one change set (a drag, a resize, a new link) | `preview` |
| The load a change would put on resources | `preview` with the `profile` option |
| Several edits in a row, each solved | `clone`, then `apply` + `solve(null)` on the copy |
| What leveling would do | `clone`, then `level` on the copy |

## Previewing an edit

Build the edit as a [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/js/api/types/scheduling-changes.md>) object, the same one you'd pass to [`apply`](<https://docs.dhtmlx.com/planning-engine/js/api/project-apply.md>), and pass it to [`preview`](<https://docs.dhtmlx.com/planning-engine/js/api/project-preview.md>).

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

const project = new Project();
project.solve({
  // All time is working time: plain day arithmetic, no weekends.
  config: { use_calendars: false },
  tasks: [
    { id: "a", start_date: "2026-01-01", duration: 2 },
    { id: "b", start_date: "2026-01-01", duration: 1 },
  ],
  links: [{ id: "ab", source: "a", target: "b", type: "fs" }],
});

// The user drags task "b" to January 5.
const drag = { tasks: { update: [{ id: "b", start_date: "2026-01-05" }] } };

const preview = project.preview(drag);
for (const update of preview.updates) {
  console.log(`task ${update.id}: ${update.start_date} .. ${update.end_date}`);
}

// The project still holds the solved dates: "b" starts on January 3.
const current = project.solve(null);
console.log(current.updates.map((u) => `${u.id} ${u.start_date}`));
```

The solve puts `b` right after `a`, on January 3. The preview prints `task b: 2026-01-05 00:00 .. 2026-01-06 00:00`. The project doesn't change: the next [`solve(null)`](<https://docs.dhtmlx.com/planning-engine/js/api/project-solve.md>) returns the cached result of the first solve, with `b` at `2026-01-03 00:00`.

When the user drops the task, send the same change set to `apply`, then call `solve(null)`. See [Editing a project](<https://docs.dhtmlx.com/planning-engine/js/live-projects/editing-a-project.md>).

The preview options take the same diagnostic switches as the solve options:

- [`subtree`](<https://docs.dhtmlx.com/planning-engine/js/api/types/common.md#subtree>) previews only one branch.
- `reasons` adds a [`reason`](<https://docs.dhtmlx.com/planning-engine/js/api/types/schedule-result.md#reason>) to each update, so you can tell why a task would move.
- [`analytics`](<https://docs.dhtmlx.com/planning-engine/js/api/types/common.md#analyticsoptions>) adds the critical path and slack of the previewed state.

Previews stay sparse: there's no `includeUnchanged` option. The full option list is on the [`preview` reference](<https://docs.dhtmlx.com/planning-engine/js/api/project-preview.md>).

## Guarding with baseline tokens

A UI that previews in the background can hit a race. Another request applies an edit or reloads the data, and the preview now runs against a baseline the user never saw. Baseline tokens catch this.

Every `solve` and `level` result carries `baselineId`, an opaque token for the accepted baseline. Pass it back as `baselineId` in the preview options, or in the change set. If the token no longer names the current baseline, `preview` throws a [`ProjectError`](<https://docs.dhtmlx.com/planning-engine/js/api/types/errors.md#projecterror>) with code `"stale-baseline"` instead of returning dates for the wrong state.

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

const project = new Project();
const loaded = project.solve({
  config: { use_calendars: false },
  tasks: [{ id: "a", start_date: "2026-01-01", duration: 2 }],
});

// The UI keeps the token of the dates it shows.
let shown = loaded.baselineId;

const resize = { tasks: { update: [{ id: "a", duration: 3 }] } };

const preview = project.preview(resize, { baselineId: shown });
// A preview reports the baseline it read, so its token works for the next preview too.
console.log(preview.baselineId === shown); // true

// Meanwhile, another edit is committed.
project.apply(resize);

// Until the next solve there is no baseline to preview against.
try {
  project.preview(resize);
} catch (error) {
  console.log(error instanceof ProjectError && error.code); // "no-baseline"
}

const latest = project.solve(null);

// The old token no longer names the baseline.
try {
  project.preview(resize, { baselineId: shown });
} catch (error) {
  if (!(error instanceof ProjectError) || error.code !== "stale-baseline") throw error;
  // Refresh the view from `latest`, then preview with its token.
  shown = latest.baselineId;
  project.preview(resize, { baselineId: shown });
}
```

A few rules for tokens:

- Take the token from the latest [`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>) result. A preview result carries the same token it read, so a chain of previews during one drag can reuse it.
- 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, leveling edits.
- Tokens are opaque. Don't parse them, store them across sessions or compare them in tests. The same data can get a new token on a repeat run.
- [`preview`](<https://docs.dhtmlx.com/planning-engine/js/api/project-preview.md>) checks `options.baselineId` and `changes.baselineId` separately. Leave both out to skip the check.

For how to handle `"stale-baseline"` and the other project errors, see [Error handling](<https://docs.dhtmlx.com/planning-engine/js/error-handling.md>).

## Preview with load

Set the [`profile`](<https://docs.dhtmlx.com/planning-engine/js/api/types/common.md#profilequery>) option to see the resource load a change would cause. The result then has [`profile`](<https://docs.dhtmlx.com/planning-engine/js/api/types/load-profile.md>), the same per-bucket load and capacity that [`loadProfile`](<https://docs.dhtmlx.com/planning-engine/js/api/project-load-profile.md>) returns, but computed on the previewed dates. Use it to show a resource histogram while the user drags a task.

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

const project = new Project();
project.solve({
  config: { use_calendars: false },
  tasks: [
    { id: "a", start_date: "2026-01-01", duration: 2 },
    { id: "b", start_date: "2026-01-03", duration: 1 },
  ],
  resources: [{ id: "dev", capacity: 1 }],
  assignments: [
    { task: "a", resource: "dev", value: 1 },
    { task: "b", resource: "dev", value: 1 },
  ],
});

// Drag "b" one day earlier, onto the last day of "a".
const drag = { tasks: { update: [{ id: "b", start_date: "2026-01-02" }] } };

const preview = project.preview(drag, {
  profile: { from: "2026-01-01", to: "2026-01-05", tasks: "overallocated" },
});

for (const resource of preview.profile.resources) {
  for (const bucket of resource.buckets) {
    if (bucket.load > bucket.capacity) {
      console.log(
        `${resource.resource} overloaded at ${bucket.start}: ` +
          `${bucket.load} of ${bucket.capacity} (${bucket.tasks.join(", ")})`,
      );
    }
  }
}
```

Both tasks need `dev` on January 2, so that bucket's load goes over its capacity of 1. The loop prints `dev overloaded at 2026-01-02 00:00: 2 of 1 (a, b)`. With `tasks: "overallocated"`, each overloaded bucket also lists the tasks that share it.

The query covers buckets that start in `[from, to)`. By default it reports every resource, with the bucket size (`basis`) derived from `config.duration_unit`. Narrow it with `resources`, or switch `basis`. The `profile` object has no `baselineId` of its own, and passing one there throws an `InputError`. Put the token in the preview options instead.

Every preview also reports `overallocations` when the project has assignments, so you can skip the profile if you only need the overloaded windows. See [Resource load](<https://docs.dhtmlx.com/planning-engine/js/resources-and-assignment/resource-load.md>) and the [`preview` reference](<https://docs.dhtmlx.com/planning-engine/js/api/project-preview.md>) for the full query fields.

## Cloning a project

[`preview`](<https://docs.dhtmlx.com/planning-engine/js/api/project-preview.md>) takes one change set. To see the result of several edits in a row, each solved before the next, [clone](<https://docs.dhtmlx.com/planning-engine/js/api/project.md#clone>) the project and work on the copy.

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

const resize = (id, duration) => ({ tasks: { update: [{ id, duration }] } });

let project = new Project();
project.solve({
  config: { use_calendars: false },
  tasks: [
    { id: "a", start_date: "2026-01-01", duration: 2 },
    { id: "b", start_date: "2026-01-01", duration: 1 },
  ],
  links: [{ id: "ab", source: "a", target: "b", type: "fs" }],
});

// An independent copy with the same baseline and options.
const draft = project.clone();
draft.apply(resize("a", 4));
draft.solve(null);
draft.apply(resize("b", 3));
const outcome = draft.solve(null);

for (const update of outcome.updates) {
  console.log(`draft task ${update.id}: ${update.start_date} .. ${update.end_date}`);
}

// The original is untouched and still previews against its own baseline.
console.log(project.preview({}).updates.length); // 0

// The user accepts the draft: keep it as the project.
project = draft;
```

The last solve on the draft prints `draft task b: 2026-01-05 00:00 .. 2026-01-08 00:00`. Edits on one copy never affect the other. The copy is a full [`Project`](<https://docs.dhtmlx.com/planning-engine/js/api/project.md>) with its own baseline, cached result and `baselineId`, and it keeps the original's `dates` option. You can preview, `level`, `evaluate` or keep editing it. To accept the draft, replace the original with it, or replay the same changes on the original with `apply`. To discard it, drop the reference.

:::note
`clone()` copies the whole project at once, so its cost grows with the project's size.
:::

Clone before [`level`](<https://docs.dhtmlx.com/planning-engine/js/api/project-level.md>) too. `level` accepts the leveled dates as the new baseline, so running it on a copy is how you show the user a leveling proposal before committing it:

```js
// `project` is a solved Project.
function levelingProposal(project) {
  const draft = project.clone();
  const leveled = draft.level();
  for (const update of leveled.updates) {
    console.log(`task ${update.id} delay ${update.leveling_delay}`);
  }
  return draft;
}
```

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

The step-by-step version is in [Preview leveling](<https://docs.dhtmlx.com/planning-engine/js/how-to/preview-leveling.md>). To compare several alternative change sets with scores, use [`evaluate`](<https://docs.dhtmlx.com/planning-engine/js/api/project-evaluate.md>) instead. It scores each candidate on a throwaway copy of its own. See [Evaluating alternatives](<https://docs.dhtmlx.com/planning-engine/js/resources-and-assignment/evaluating-alternatives.md>).
