[Canonical page](<https://docs.dhtmlx.com/planning-engine/java/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/java/api/project/project.md>) holds one **accepted baseline**: the dates from its last [`solve`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) or [`level`](<https://docs.dhtmlx.com/planning-engine/java/api/project-level.md>). Every preview starts from that baseline.

[`Project#preview(changes, options)`](<https://docs.dhtmlx.com/planning-engine/java/api/project-preview.md>) copies the baseline, applies a [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/scheduling-changes.md>) value to the copy and schedules it. When the call returns, the copy is gone. 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.

You get back a regular [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/java/api/results/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 the `solve(null, …)` after an [`apply`](<https://docs.dhtmlx.com/planning-engine/java/api/project-apply.md>) reports them. 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 cycle.
- `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 [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>) for the first rejected operation, with its `path` and message. It also needs a clean, solved baseline. Before the first solve, and after any `apply` until the next solve, it throws [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/project-exception.md>) with code `NO_BASELINE`.

For a change that takes more than one step, or for a `level` run, use [`Project#copy()`](<https://docs.dhtmlx.com/planning-engine/java/api/project/project.md#copy>) instead. A copy is O(1): it shares state with the original, and the first edit on either side gives that side its own data. Edit, solve or level the copy, 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 [`PreviewOptions.profile`](<https://docs.dhtmlx.com/planning-engine/java/api/project/preview-options.md#profile>) |
| Several edits in a row, each solved | `copy()`, then `apply` + `solve(null, …)` on the copy |
| What leveling would do | `copy()`, then `level` on the copy |

## Previewing an edit

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

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

public class PreviewDrag {
  static Moment day(int d) {
    return Moment.fromYmdHm(2026, 1, d, 0, 0);
  }

  static TaskData task(String id, int start, double duration) {
    return new TaskData(Id.str(id)).withStartDate(day(start)).withDuration(duration);
  }

  public static void main(String[] args) throws Exception {
    ProjectData data = new ProjectData()
        .withTasks(List.of(task("a", 1, 2.0), task("b", 1, 1.0)))
        .withLinks(List.of(new LinkData()
            .withId(Id.str("ab")).withSource(Id.str("a")).withTarget(Id.str("b"))));
    // All time is working time: plain day arithmetic, no weekends.
    data.config.useCalendars = false;

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

    // The user drags task "b" to January 5.
    SchedulingChanges drag = new SchedulingChanges();
    drag.tasks.update.add(new TaskUpdate(Id.str("b")).withStartDate(Patch.of(day(5))));

    ScheduleResult preview = project.preview(drag, new PreviewOptions());
    for (Mutation update : preview.updates) {
      System.out.println("task " + update.id + ": " + update.startDate + " .. " + update.endDate);
    }

    // The project still holds the solved dates: "b" starts on January 3.
    ScheduleResult current = project.solve(null, new SolveOptions());
    System.out.println("unchanged: " + (current == solved));
  }
}
```

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/java/api/project-solve.md>) returns the cached result of the first solve, the same object, so the last line prints `unchanged: true`.

When the user drops the task, send the same `SchedulingChanges` to `apply` and then call `solve(null, …)`. See [Data Updates](<https://docs.dhtmlx.com/planning-engine/java/live-projects/editing-a-project.md>).

[`PreviewOptions`](<https://docs.dhtmlx.com/planning-engine/java/api/project/preview-options.md>) takes the same diagnostic switches as [`SolveOptions`](<https://docs.dhtmlx.com/planning-engine/java/api/project/solve-options.md>):

- `withSubtree(new Subtree(id))` previews only one branch.
- `withReasons(true)` adds a [`Reason`](<https://docs.dhtmlx.com/planning-engine/java/api/results/reason.md>) to each update, which tells you why a task would move.
- `withAnalytics(...)` adds the critical path and slack of the previewed state.

Previews stay sparse: there's no `includeUnchanged` option. The full list is on the [`PreviewOptions` reference](<https://docs.dhtmlx.com/planning-engine/java/api/project/preview-options.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`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#baselineid>), an opaque token for the accepted baseline. Pass it back with [`PreviewOptions.withBaselineId(...)`](<https://docs.dhtmlx.com/planning-engine/java/api/project/preview-options.md#baselineid>) (or in [`SchedulingChanges.baselineId`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/scheduling-changes.md#baselineid>)). If the token no longer names the current baseline, [`preview`](<https://docs.dhtmlx.com/planning-engine/java/api/project-preview.md>) throws [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/project-exception.md>) with code `STALE_BASELINE` instead of returning dates for the wrong state.

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

public class GuardedPreview {
  public static void main(String[] args) throws Exception {
    ProjectData data = new ProjectData()
        .withTasks(List.of(new TaskData(Id.str("a"))
            .withStartDate(Moment.fromYmdHm(2026, 1, 1, 0, 0))
            .withDuration(2.0)));
    data.config.useCalendars = false;

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

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

    SchedulingChanges resize = new SchedulingChanges();
    resize.tasks.update.add(new TaskUpdate(Id.str("a")).withDuration(Patch.of(3.0)));

    PreviewOptions guarded = new PreviewOptions().withBaselineId(shown);
    ScheduleResult preview = project.preview(resize, guarded);
    // A preview reports the baseline it read, so its token works for the next preview too.
    System.out.println("same token: " + preview.baselineId.equals(shown));

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

    // Until the next solve there is no baseline to preview against.
    try {
      project.preview(resize, new PreviewOptions());
    } catch (ProjectException e) {
      System.out.println(e.code); // NO_BASELINE
    }

    ScheduleResult latest = project.solve(null, options);

    // The old token no longer names the baseline.
    try {
      project.preview(resize, guarded);
    } catch (ProjectException e) {
      if (e.code != ProjectError.STALE_BASELINE) throw e;
      // Refresh the view from `latest`, then preview with its token.
      project.preview(resize, new PreviewOptions().withBaselineId(latest.baselineId));
    }
  }
}
```

A few rules for tokens:

- Take the token from the latest [`solve`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) or [`level`](<https://docs.dhtmlx.com/planning-engine/java/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/java/api/project-apply.md>), a solve that moves dates, leveling edits.
- Tokens are opaque strings. Don't parse them, store them across runs or compare them between processes. The same data can get a new token on a repeat run.
- [`preview`](<https://docs.dhtmlx.com/planning-engine/java/api/project-preview.md>) checks `PreviewOptions.baselineId` and `SchedulingChanges.baselineId` separately. Leave both `null` to skip the check.

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

## Preview with load

Set [`PreviewOptions.profile`](<https://docs.dhtmlx.com/planning-engine/java/api/project/preview-options.md#profile>) to a [`ProfileQuery`](<https://docs.dhtmlx.com/planning-engine/java/api/resources/profile-query.md>) to see the resource load a change would cause. The result then has `profile`, the same per-bucket load and capacity that [`loadProfile`](<https://docs.dhtmlx.com/planning-engine/java/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.

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

public class PreviewLoad {
  static Moment day(int d) {
    return Moment.fromYmdHm(2026, 1, d, 0, 0);
  }

  static TaskData task(String id, int start, double duration) {
    return new TaskData(Id.str(id)).withStartDate(day(start)).withDuration(duration);
  }

  static AssignmentData assign(String task) {
    return new AssignmentData()
        .withTask(Id.str(task)).withResource(Id.str("dev")).withValue(1.0);
  }

  public static void main(String[] args) throws Exception {
    ProjectData data = new ProjectData()
        .withTasks(List.of(task("a", 1, 2.0), task("b", 3, 1.0)))
        .withResources(List.of(new ResourceData().withId(Id.str("dev")).withCapacity(1.0)))
        .withAssignments(List.of(assign("a"), assign("b")));
    data.config.useCalendars = false;

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

    // Drag "b" one day earlier, onto the last day of "a".
    SchedulingChanges drag = new SchedulingChanges();
    drag.tasks.update.add(new TaskUpdate(Id.str("b")).withStartDate(Patch.of(day(2))));

    PreviewOptions options = new PreviewOptions()
        .withProfile(new ProfileQuery(day(1), day(5)).withTasks(ProfileTasks.OVER));
    ScheduleResult preview = project.preview(drag, options);

    if (preview.profile != null) {
      for (ResourceLoad resource : preview.profile.resources) {
        for (LoadBucket bucket : resource.buckets) {
          if (bucket.load > bucket.capacity) {
            System.out.println(resource.resource + " overloaded at " + bucket.start + ": "
                + bucket.load + " of " + bucket.capacity + " " + bucket.tasks);
          }
        }
      }
    }
  }
}
```

Both tasks need `dev` on January 2, so that bucket's load goes over its capacity of 1. With [`ProfileTasks.OVER`](<https://docs.dhtmlx.com/planning-engine/java/api/resources/profile-tasks.md>), 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, and the bucket size (`basis`) comes from `config.durationUnit`. Narrow it with `withResources(...)`, or set `withBasis(...)`. `ProfileQuery` has no `baselineId` of its own: the preview checks `PreviewOptions.baselineId`. Every preview also reports [`overallocations`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#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/java/resources-and-assignment/resource-load.md>) and the [`ProfileQuery` reference](<https://docs.dhtmlx.com/planning-engine/java/api/resources/profile-query.md>).

## Copying a project

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

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

public class DraftCopy {
  static Moment day(int d) {
    return Moment.fromYmdHm(2026, 1, d, 0, 0);
  }

  static TaskData task(String id, int start, double duration) {
    return new TaskData(Id.str(id)).withStartDate(day(start)).withDuration(duration);
  }

  static SchedulingChanges resize(String id, double duration) {
    SchedulingChanges changes = new SchedulingChanges();
    changes.tasks.update.add(new TaskUpdate(Id.str(id)).withDuration(Patch.of(duration)));
    return changes;
  }

  public static void main(String[] args) throws Exception {
    ProjectData data = new ProjectData()
        .withTasks(List.of(task("a", 1, 2.0), task("b", 1, 1.0)))
        .withLinks(List.of(new LinkData()
            .withId(Id.str("ab")).withSource(Id.str("a")).withTarget(Id.str("b"))));
    data.config.useCalendars = false;

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

    // A cheap copy: it shares state with `project` until one of them is edited.
    Project draft = project.copy();
    draft.apply(resize("a", 4.0));
    draft.solve(null, options);
    draft.apply(resize("b", 3.0));
    ScheduleResult outcome = draft.solve(null, options);

    for (Mutation update : outcome.updates) {
      System.out.println("draft task " + update.id + ": " + update.startDate + " .. " + update.endDate);
    }

    // The original is untouched and still previews against its own baseline.
    boolean clean = project.preview(new SchedulingChanges(), new PreviewOptions()).updates.isEmpty();
    System.out.println("original unchanged: " + clean);

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

Edits on one copy never affect the other. The copy is a full [`Project`](<https://docs.dhtmlx.com/planning-engine/java/api/project/project.md>) with its own baseline, cache and `baselineId`, so you can preview it, `level` it, `evaluate` against it 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.

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

```java
static void levelingProposal(Project project) {
  Project draft = project.copy();
  LevelingResult leveled = draft.level(new LevelingOptions());
  for (Mutation update : leveled.schedule.updates) {
    System.out.println("task " + update.id + " delay " + update.levelingDelay);
  }
}
```

The step-by-step version is in [Preview leveling](<https://docs.dhtmlx.com/planning-engine/java/how-to/preview-leveling.md>). To compare several alternative change sets with scores, use [`evaluate`](<https://docs.dhtmlx.com/planning-engine/java/api/project-evaluate.md>) instead. It works on disposable copies internally. See [Evaluating alternatives](<https://docs.dhtmlx.com/planning-engine/java/resources-and-assignment/evaluating-alternatives.md>).

Copies also help with threads. A `Project` isn't synchronized, and even [`copy()`](<https://docs.dhtmlx.com/planning-engine/java/api/project/project.md#copy>) updates sharing flags on the original. To use one project from several threads, guard it with a lock, or make one copy per thread on a single thread and hand each thread its own. See [Share a project between threads](<https://docs.dhtmlx.com/planning-engine/java/how-to/share-a-project-between-threads.md>).
