[Canonical page](<https://docs.dhtmlx.com/planning-engine/java/how-to/recover-from-rejected-changes/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Recover from rejected changes

Get an editor working again after [`apply`](<https://docs.dhtmlx.com/planning-engine/java/api/project-apply.md>) rejects part of a change set, without restarting the app or losing the user's edits.

When `apply` rejects an operation, the project sets [`needsResync`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/apply-report.md#needsresync>). From then on, `solve(null, …)`, `apply` and `level` throw [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/project-exception.md>) with code [`ProjectError.NEEDS_RESYNC`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/project-exception.md#projecterror>). Reads that need a baseline (`preview`, `loadProfile`, `evaluate`, `suggestAssignments`) throw `ProjectError.NO_BASELINE`. To clear the flag, reload the full data with `solve(data, …)`. See [Editing a project](<https://docs.dhtmlx.com/planning-engine/java/live-projects/editing-a-project.md#rejections-and-resync>) for why.

## Prerequisites

- A long-lived [`Project`](<https://docs.dhtmlx.com/planning-engine/java/api/project/project.md>) solved once with [`solve(data, …)`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>).
- Your own model that can produce the full [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/java/api/data/project-data.md>) the user expects to see. You resync from this data.

## Steps

1. Apply the change set and keep the [`ApplyReport`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/apply-report.md>):

   ```java
   var report = project.apply(changes);
   ```

   If `apply` throws a `ProjectException`, nothing was applied: `NEEDS_RESYNC` comes from an earlier rejection, `STALE_BASELINE` from a `baselineId` that isn't current. A rejection inside the batch doesn't throw.

2. Check [`needsResync`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/apply-report.md#needsresync>). If it's `false`, continue as usual with [`solve(null, …)`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>):

   ```java
   if (!report.needsResync) {
     project.solve(null, new SolveOptions());
   }
   ```

3. Show the user what was refused. Each [`RejectedOperation`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/rejected-operation.md>) has the operation's `path` (such as `tasks.remove[0]`) and a `message`:

   ```java
   for (var op : report.rejected) {
     System.err.println("rejected " + op.path + ": " + op.message);
   }
   ```

   The other operations in the batch were applied. A rejected operation leaves no partial edit behind.

4. Make your model hold the state you want. Undo the rejected edits in it, or keep it as is if it's already correct. Don't try to repair the project with more [`apply`](<https://docs.dhtmlx.com/planning-engine/java/api/project-apply.md>) calls: they throw `NEEDS_RESYNC`.

5. Resync with the full data. Set `includeUnchanged` so the result carries the dates of every task, not only the ones that moved:

   ```java
   var resync = new SolveOptions().withIncludeUnchanged(true);
   var result = project.solve(model, resync);
   ```

   `solve` deep-copies `model`, so later edits to your model don't leak into the project.

6. Write every entry of `result.updates` back into your UI. The engine's state may differ from what the UI shows, so refresh all task dates, not just the edited ones.

7. Take the new `baselineId` from `result` for the next [`preview`](<https://docs.dhtmlx.com/planning-engine/java/api/project-preview.md>) or change set. Tokens from before the resync are stale.

The full example below stretches one task and, in the same batch, removes a task the engine doesn't have:

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

public class RecoverFromRejectedChanges {
  public static void main(String[] args) throws Exception {
    var jan5 = Moment.fromYmdHm(2026, 1, 5, 0, 0);

    // The editor's own model: the data you resync from.
    var model = new ProjectData()
        .withTasks(List.of(new TaskData(Id.str("design")).withStartDate(jan5).withDuration(2.0)));
    // 24/7 time keeps the example dates easy to check.
    model.config.useCalendars = false;

    var project = new Project();
    project.solve(model, new SolveOptions());

    // The user stretches "design" to 4 days. The UI also sends a removal
    // of "review", a task the engine never had.
    model.tasks.get(0).duration = 4.0;
    var changes = new SchedulingChanges();
    changes.tasks.update.add(new TaskUpdate(Id.str("design")).withDuration(Patch.of(4.0)));
    changes.tasks.remove.add(Id.str("review"));

    var report = project.apply(changes);
    if (!report.needsResync) {
      project.solve(null, new SolveOptions());
      return;
    }

    for (var op : report.rejected) {
      System.err.println("rejected " + op.path + ": " + op.message);
    }

    var resync = new SolveOptions().withIncludeUnchanged(true);
    try {
      var result = project.solve(model, resync);
      for (var m : result.updates) {
        System.out.println(m.id + " " + m.startDate + ".." + m.endDate);
      }
    } catch (InputException e) {
      // The project keeps its state and still needs a resync.
      System.err.println("resync data invalid at " + e.path + ": " + e.getMessage());
    }
  }
}
```

## When the resync data is invalid

The resync goes through the same validation as the first load. If your model produces invalid data, such as a duplicate task id, [`solve(data, …)`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) throws [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>) with the field path in `path`, for example `tasks[1].id`. The project keeps its previous state and still throws `NEEDS_RESYNC`. Fix the data and call `solve(data, …)` again. See [Error handling](<https://docs.dhtmlx.com/planning-engine/java/error-handling.md>) for the full list of validation errors.

## When you didn't check the report

If some code path skipped `needsResync`, the next `apply` or `solve(null, …)` throws. [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/project-exception.md>) is unchecked, so catch it where you send edits and resync on `NEEDS_RESYNC`:

```java
try {
  project.apply(changes);
  project.solve(null, new SolveOptions());
} catch (ProjectException e) {
  if (e.code != ProjectError.NEEDS_RESYNC) throw e;
  project.solve(model, new SolveOptions().withIncludeUnchanged(true));
}
```

`apply` checks `NEEDS_RESYNC` before anything else, so the change set in that call isn't applied. Put its edits in your model before the resync.

## Drop the edit instead of resyncing

To avoid a full reload, apply the change set to a copy first. [`copy()`](<https://docs.dhtmlx.com/planning-engine/java/api/project/project.md#copy>) is cheap, and a rejection on the copy doesn't touch the original:

```java
var draft = project.copy();
var report = draft.apply(changes);
if (report.needsResync) {
  // Drop `draft`. `project` still has its baseline; roll the edit back in your model too.
} else {
  draft.solve(null, new SolveOptions());
  project = draft;
}
```

Use this when the user should either get the whole change set or none of it.

## Result

The example prints the rejected removal on stderr and the refreshed date of every task on stdout:

```text
rejected tasks.remove[0]: unknown task id
design 2026-01-05 00:00..2026-01-09 00:00
```

After the resync, [`apply`](<https://docs.dhtmlx.com/planning-engine/java/api/project-apply.md>), [`solve(null, …)`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>), `level` and [`preview`](<https://docs.dhtmlx.com/planning-engine/java/api/project-preview.md>) work again, and `report.needsResync` is `false` for the next valid change set.
