[Canonical page](<https://docs.dhtmlx.com/planning-engine/java/resources-and-assignment/evaluating-alternatives/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Evaluating alternatives

This guide shows how to compare several possible edits in one call and pick the best one, for example which resource should take a task. It covers building candidates, scoring them with and without leveling, reading the metrics, handling candidates that fail, scoring on several threads and committing the winner.

## How evaluation works

[`Project#evaluate`](<https://docs.dhtmlx.com/planning-engine/java/api/project-evaluate.md>) takes a list of **candidates**. A [`Candidate`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/candidate.md>) is an `id` plus a [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/scheduling-changes.md>) value, the same change set you'd pass to [`apply`](<https://docs.dhtmlx.com/planning-engine/java/api/project-apply.md>). Each candidate is one alternative: "give task B to dev2", "split the review between two people", "add a link". The call scores every candidate against the project's accepted baseline and returns an [`EvaluateResult`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/evaluate-result.md>). It never modifies the project, just like [`preview`](<https://docs.dhtmlx.com/planning-engine/java/api/project-preview.md>).

Every candidate goes through the same run on a disposable copy of the project:

1. Copy the reference state. That's the accepted baseline, or its cleared copy with [`clearFirst`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/evaluate-options.md#clearfirst>) (see [Options](<https://docs.dhtmlx.com/planning-engine/java/resources-and-assignment/evaluating-alternatives.md#options>)).
2. `apply` the candidate's changes.
3. [`solve(null, …)`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) and measure the **scheduled** stage.
4. When [`EvaluateOptions.level`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/evaluate-options.md#level>) is set, `level` with those options and measure the **leveled** stage.

You'd run the same sequence to commit the candidate for real. Each candidate starts from the reference state, never from the previous candidate, and both stages are compared with the one baseline.

The result has three parts:

- `baseline`: the metrics of the reference state itself. All its deltas are 0.
- `candidates`: one [`CandidateResult`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/candidate-result.md>) per candidate, in input order. It holds `scheduled` metrics, `leveled` metrics (only with `level`) and a `cost`.
- `baselineId`: the token of the accepted baseline the candidates were scored against.

Each stage is described by a [`Metrics`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/metrics.md>) object: the project finish and the finish of each focus task, resource overload, conflicts, cycles and a single `cost` number. Every measurement is an absolute `value` plus a `delta` against the baseline, so you can show either one.

Errors come in two levels. Problems with the call itself throw and fail the whole call: no solved baseline ([`ProjectException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/project-exception.md>) with code `NO_BASELINE`), a stale `baselineId` in the options or in any candidate (code `STALE_BASELINE`), an unknown focus task or leveling options that don't fit the baseline ([`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>)). Everything else fails only its own candidate: the candidate gets an `error` and no metrics, and the other candidates are scored as usual.

`evaluate` reports scores, not dates. The result has no task updates and no overallocation windows. Once you've picked a candidate, commit it yourself with `apply` and `level`, or look at its dates with `preview`.

## Comparing candidates

All examples on this page use the same small project: tasks A and B, two working days each, both starting on Monday, August 10, 2026, and both assigned to `dev1`. The resource `dev2` is idle, and `dev1` is overloaded for two days.

### Candidates

Build each alternative as a `Candidate` and pass them all to [`evaluate`](<https://docs.dhtmlx.com/planning-engine/java/api/project-evaluate.md>). An empty change set is a useful candidate too: it scores "leave the plan as it is" next to the other options.

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

public class CompareCandidates {
    static TaskData task(String id, Moment start, double duration) {
        return new TaskData(Id.str(id)).withStartDate(start).withDuration(duration);
    }

    static AssignmentData assign(String id, String task, String resource) {
        return new AssignmentData()
            .withId(Id.str(id))
            .withTask(Id.str(task))
            .withResource(Id.str(resource))
            .withValue(1.0);
    }

    public static void main(String[] args) throws Exception {
        // 2026-08-10 is a Monday.
        Moment monday = Moment.fromYmdHm(2026, 8, 10, 8, 0);
        ProjectData data = new ProjectData()
            .withTasks(List.of(task("A", monday, 2.0), task("B", monday, 2.0)))
            .withResources(List.of(
                new ResourceData().withId(Id.str("dev1")),
                new ResourceData().withId(Id.str("dev2"))))
            .withAssignments(List.of(assign("aA", "A", "dev1"), assign("aB", "B", "dev1")));
        Project project = new Project();
        project.solve(data, new SolveOptions());

        // Candidate 1: keep the plan as it is.
        Candidate stay = new Candidate()
            .withId(Id.str("stay"))
            .withChanges(new SchedulingChanges());

        // Candidate 2: give task B to dev2.
        SchedulingChanges toDev2 = new SchedulingChanges();
        toDev2.assignments.update.add(
            new AssignmentUpdate(Id.str("aB")).withResource(Patch.of(Id.str("dev2"))));
        Candidate dev2 = new Candidate().withId(Id.str("dev2")).withChanges(toDev2);

        EvaluateOptions options = new EvaluateOptions().withFocus(List.of(Id.str("B")));
        EvaluateResult result = project.evaluate(List.of(stay, dev2), options);

        System.out.println("baseline: cost " + result.baseline.cost);
        for (CandidateResult candidate : result.candidates) {
            System.out.println(candidate.id + ": cost " + candidate.cost);
        }
    }
}
```

```text
baseline: cost 2.0
stay: cost 2.0
dev2: cost 0.0
```

Without leveling, only the scheduled stage is scored. `stay` keeps both tasks on `dev1` and costs 2, two resource-days of overload. `dev2` removes the overload and costs 0.

`evaluate` takes a `Vec<Candidate>` or any `Collection<Candidate>`, so `List.of(...)` works. The candidate `id` is echoed back so you can match results to inputs. Ids aren't checked for uniqueness, and results keep input order anyway. A candidate needs a `changes` value; `new Candidate()` leaves it `null`. See the [`Candidate` reference](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/candidate.md>).

### Options

[`EvaluateOptions`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/evaluate-options.md>) has four fields, all with defaults:

- `level`: a [`LevelingOptions`](<https://docs.dhtmlx.com/planning-engine/java/api/resources/leveling-options.md>) levels each candidate after scheduling it and adds the leveled stage. `null` (the default) scores the scheduled stage only.
- [`focus`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/evaluate-options.md#focus>): the tasks to report finish dates and focus overload for. Nothing is added implicitly, so list the tasks your candidates are about.
- `baselineId`: a baseline token. The call throws a `ProjectException` with code `STALE_BASELINE` when it no longer names the accepted baseline. Each candidate's `changes.baselineId` is checked the same way.
- `clearFirst`: score against a copy with the in-scope `levelingDelay` and `pauses` cleared. Default `false`.

Leveling changes the scores. Without it, an overloaded plan scores badly on overload. With it, the overload turns into delay, which is what the user gets after committing and leveling:

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

public class LeveledStage {
    static TaskData task(String id, Moment start, double duration) {
        return new TaskData(Id.str(id)).withStartDate(start).withDuration(duration);
    }

    static AssignmentData assign(String id, String task, String resource) {
        return new AssignmentData()
            .withId(Id.str(id))
            .withTask(Id.str(task))
            .withResource(Id.str(resource))
            .withValue(1.0);
    }

    /** Tasks A and B, two days each, both on dev1 in the same week. */
    static Project solvedProject() {
        Moment monday = Moment.fromYmdHm(2026, 8, 10, 8, 0);
        ProjectData data = new ProjectData()
            .withTasks(List.of(task("A", monday, 2.0), task("B", monday, 2.0)))
            .withResources(List.of(
                new ResourceData().withId(Id.str("dev1")),
                new ResourceData().withId(Id.str("dev2"))))
            .withAssignments(List.of(assign("aA", "A", "dev1"), assign("aB", "B", "dev1")));
        Project project = new Project();
        project.solve(data, new SolveOptions());
        return project;
    }

    static SchedulingChanges reassign(String assignment, String resource) {
        SchedulingChanges changes = new SchedulingChanges();
        changes.assignments.update.add(
            new AssignmentUpdate(Id.str(assignment)).withResource(Patch.of(Id.str(resource))));
        return changes;
    }

    static Candidate candidate(String id, SchedulingChanges changes) {
        return new Candidate().withId(Id.str(id)).withChanges(changes);
    }

    public static void main(String[] args) throws Exception {
        Project project = solvedProject();
        List<Candidate> candidates = List.of(
            candidate("stay", new SchedulingChanges()),
            candidate("dev2", reassign("aB", "dev2")));
        EvaluateOptions options = new EvaluateOptions()
            .withLevel(new LevelingOptions())
            .withFocus(List.of(Id.str("A"), Id.str("B")));
        EvaluateResult result = project.evaluate(candidates, options);

        for (CandidateResult candidate : result.candidates) {
            if (candidate.leveled == null) continue;
            Metrics leveled = candidate.leveled.metrics;
            Shift end = leveled.finish.project;
            if (end != null) {
                System.out.printf("%s: project ends %s (%+.0f days), cost %s%n",
                    candidate.id, end.date, end.delta, candidate.cost);
            }
            for (TaskFinish task : leveled.finish.tasks) {
                if (task.finish != null) {
                    System.out.printf("  %s ends %s (%+.0f)%n",
                        task.id, task.finish.date, task.finish.delta);
                }
            }
        }
    }
}
```

```text
stay: project ends 2026-08-14 08:00 (+2 days), cost 2.0
  A ends 2026-08-12 08:00 (+0)
  B ends 2026-08-14 08:00 (+2)
dev2: project ends 2026-08-12 08:00 (+0 days), cost 0.0
  A ends 2026-08-12 08:00 (+0)
  B ends 2026-08-12 08:00 (+0)
```

For `stay`, leveling resolves the overload by pushing B two days later, so the cost is now the delay. For `dev2`, there's nothing to level.

`level` runs with the options exactly as you pass them. [`LevelingOptions.clearFirst`](<https://docs.dhtmlx.com/planning-engine/java/api/resources/leveling-options.md#clearfirst>) defaults to `true`, so each candidate is re-leveled from zero. That's a trap after an earlier `level` run. The stored delays are already in the baseline finish, and they hide the overload the current assignments cause. A candidate that removes the overload then shows a negative finish delta, because leveling from zero drops the old delays.

[`EvaluateOptions.clearFirst`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/evaluate-options.md#clearfirst>) fixes this. It replaces the reference state with a copy that has the in-scope delays and pauses cleared, then solves that copy. The baseline metrics and every candidate run use the copy. The scope is `level.scope.tasks` when you set it, otherwise every task. The flag is independent of `LevelingOptions.clearFirst`, so it also works without `level`.

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

public class ClearFirst {
    static TaskData task(String id, Moment start, double duration) {
        return new TaskData(Id.str(id)).withStartDate(start).withDuration(duration);
    }

    static AssignmentData assign(String id, String task, String resource) {
        return new AssignmentData()
            .withId(Id.str(id))
            .withTask(Id.str(task))
            .withResource(Id.str(resource))
            .withValue(1.0);
    }

    /** Tasks A and B, two days each, both on dev1 in the same week. */
    static Project solvedProject() {
        Moment monday = Moment.fromYmdHm(2026, 8, 10, 8, 0);
        ProjectData data = new ProjectData()
            .withTasks(List.of(task("A", monday, 2.0), task("B", monday, 2.0)))
            .withResources(List.of(
                new ResourceData().withId(Id.str("dev1")),
                new ResourceData().withId(Id.str("dev2"))))
            .withAssignments(List.of(assign("aA", "A", "dev1"), assign("aB", "B", "dev1")));
        Project project = new Project();
        project.solve(data, new SolveOptions());
        return project;
    }

    static SchedulingChanges reassign(String assignment, String resource) {
        SchedulingChanges changes = new SchedulingChanges();
        changes.assignments.update.add(
            new AssignmentUpdate(Id.str(assignment)).withResource(Patch.of(Id.str(resource))));
        return changes;
    }

    static Candidate candidate(String id, SchedulingChanges changes) {
        return new Candidate().withId(Id.str(id)).withChanges(changes);
    }

    public static void main(String[] args) throws Exception {
        Project project = solvedProject();
        // The plan was leveled earlier: B carries a two-day leveling delay.
        LevelingResult leveled = project.level(new LevelingOptions());

        List<Candidate> candidates = List.of(candidate("dev2", reassign("aB", "dev2")));
        EvaluateOptions options = new EvaluateOptions()
            .withBaselineId(leveled.schedule.baselineId)
            .withLevel(new LevelingOptions())
            .withClearFirst(true);
        EvaluateResult result = project.evaluate(candidates, options);

        // The cleared baseline shows the overload that B's delay was hiding.
        System.out.println("baseline overload: " + result.baseline.overload.total.value);
        for (CandidateResult candidate : result.candidates) {
            if (candidate.leveled != null && candidate.leveled.metrics.finish.project != null) {
                System.out.printf("%s: finish delta %+.0f%n",
                    candidate.id, candidate.leveled.metrics.finish.project.delta);
            }
        }
    }
}
```

```text
baseline overload: 2.0
dev2: finish delta +0
```

With `clearFirst`, the baseline reports 2 resource-days of overload and the `dev2` candidate a finish delta of 0. Without it, the baseline has no overload and the same candidate shows a finish delta of -2. Both views are valid. Use `clearFirst` when you'll commit with `level` and `LevelingOptions.clearFirst`, which re-levels from zero, because that's the plan it scores.

A few details:

- `baselineId`, candidate tokens, `level` options and focus ids are all checked against the accepted state before anything is cleared. `clearFirst` adds no new errors.
- A candidate's own `changes.baselineId` is dropped before its trial run, because the cleared copy has a different token. The result's `baselineId` is still the accepted state's.
- When there's nothing to clear in scope, the output is the same as with `clearFirst` set to `false`.

The full field list is on the [`EvaluateOptions` reference](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/evaluate-options.md>). For the leveling options themselves, see [Leveling resources](<https://docs.dhtmlx.com/planning-engine/java/resources-and-assignment/leveling-resources.md>).

### Metrics and cost

Every stage reports the same `Metrics`:

| Field | What it measures |
|---|---|
| `finish.project` | Latest end over scheduled tasks. `null` when the stage has no tasks. |
| `finish.tasks` | One [`TaskFinish`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/metrics.md#taskfinish>) per focus task, in focus order. |
| `overload.total` | Overload over all resources. |
| `overload.resources` | Overload per resource, for every resource that's non-zero in the stage or the baseline. |
| `overload.focus` | Per focus task: the overload per resource in the buckets that intersect the task. |
| `conflicts`, `cycles` | How many conflicts and cycles the stage has. |
| `cost` | One number for ranking. Lower is better. |

Dates come as a [`Shift`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/metrics.md#shift>): `date` plus `delta`, the working time from the baseline finish in [`config.durationUnit`](<https://docs.dhtmlx.com/planning-engine/java/api/data/scheduling-config.md#durationunit>), positive when later. A task's delta is measured on its own calendar, the project's on the global calendar. Counts and overload come as an [`Amount`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/overload.md#amount>): `value` plus `delta`, both `double`.

Overload is measured the way the leveler sees it. For each time bucket, it's how far a resource's load goes over its capacity, summed over the buckets. The unit is capacity × buckets, so on a day basis 2.0 means "two resource-days over". One bucket size (basis) applies to the baseline and to every stage of every candidate: `level.basis` when you set it, otherwise the one derived from `config.durationUnit`. The same per-resource sum is what [`loadProfile`](<https://docs.dhtmlx.com/planning-engine/java/api/project-load-profile.md>) shows as excess at that basis.

Focus overload shows where the trouble is. At the scheduled stage, it's the overload around the task. At the leveled stage, it's what leveling couldn't resolve there. A milestone has zero length and covers no bucket.

The leveled stage is a [`LeveledMetrics`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/leveled-metrics.md>) object: the same `Metrics` in its `metrics` field, plus `converged`, `rounds` and `windows`, the number of overallocation windows left after leveling.

`cost` is one fixed heuristic:

```text
cost = max(0, finish.project.delta)
     + overload.total.value
     + 1000 × (conflicts.value + cycles.value)
```

When the basis follows `durationUnit`, a day of delay and a resource-day of overload weigh the same. An earlier finish doesn't earn a bonus. The 1000 penalty keeps any broken schedule, with conflicts or cycles, behind every valid one. [`CandidateResult.cost`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/candidate-result.md>) is a `Double` holding the cost of the last stage present: leveled when you level, scheduled otherwise. The baseline carries its own `cost`.

If the heuristic doesn't fit your product, rank on the raw values instead. This method prints the per-resource overload change of each candidate's scheduled stage:

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

public class PrintOverload {
    static void printOverload(EvaluateResult result) {
        for (CandidateResult candidate : result.candidates) {
            if (candidate.scheduled == null) continue;
            for (ResourceOverload row : candidate.scheduled.overload.resources) {
                System.out.printf("%s: %s overload %s (%+.1f vs baseline)%n",
                    candidate.id, row.resource, row.overload.value, row.overload.delta);
            }
        }
    }
}
```

For the project above, the `dev2` candidate prints a row for `dev1` with a delta of -2. The row is there because `dev1` was overloaded in the baseline, even though it's zero in the stage. Resources come in stage order, then resources that exist only in the baseline. See the [`Metrics` reference](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/metrics.md>) and the [`Overload` reference](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/overload.md>).

### Failed and cyclic candidates

A candidate can fail without failing the call. Three cases need handling:

- **Rejected.** [`apply`](<https://docs.dhtmlx.com/planning-engine/java/api/project-apply.md>) rejects an operation in the change set (only the first rejection is reported), or [`solve`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) or `level` fails on the candidate's input, for example on a leveling scope id the candidate removed. The result's `error` holds a [`RejectedOperation`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/rejected-operation.md>) with a `path` and a `message`. `scheduled`, `leveled` and `cost` are `null`.
- **Cyclic.** The candidate creates a dependency cycle. `scheduled` is measured on the unchanged dates the cyclic solve returns, and `cycles.value` is above 0. Leveling stops at round 0 on a cyclic plan, so `leveled` is `null`. The cycle penalty pushes the cost over 1000.
- **Focus task removed.** When the candidate removes a focus task, that task's `finish` and its focus overload `resources` are `null`. The rest of the metrics are normal.

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

public class FailedCandidates {
    static TaskData task(String id, Moment start, double duration) {
        return new TaskData(Id.str(id)).withStartDate(start).withDuration(duration);
    }

    static AssignmentData assign(String id, String task, String resource) {
        return new AssignmentData()
            .withId(Id.str(id))
            .withTask(Id.str(task))
            .withResource(Id.str(resource))
            .withValue(1.0);
    }

    /** Tasks A and B, two days each, both on dev1 in the same week. */
    static Project solvedProject() {
        Moment monday = Moment.fromYmdHm(2026, 8, 10, 8, 0);
        ProjectData data = new ProjectData()
            .withTasks(List.of(task("A", monday, 2.0), task("B", monday, 2.0)))
            .withResources(List.of(
                new ResourceData().withId(Id.str("dev1")),
                new ResourceData().withId(Id.str("dev2"))))
            .withAssignments(List.of(assign("aA", "A", "dev1"), assign("aB", "B", "dev1")));
        Project project = new Project();
        project.solve(data, new SolveOptions());
        return project;
    }

    static SchedulingChanges reassign(String assignment, String resource) {
        SchedulingChanges changes = new SchedulingChanges();
        changes.assignments.update.add(
            new AssignmentUpdate(Id.str(assignment)).withResource(Patch.of(Id.str(resource))));
        return changes;
    }

    static Candidate candidate(String id, SchedulingChanges changes) {
        return new Candidate().withId(Id.str(id)).withChanges(changes);
    }

    static LinkAdd link(String id, String source, String target) {
        return new LinkAdd().withId(Id.str(id)).withSource(Id.str(source)).withTarget(Id.str(target));
    }

    public static void main(String[] args) throws Exception {
        Project project = solvedProject();

        SchedulingChanges cycle = new SchedulingChanges();
        cycle.links.add.addAll(List.of(link("ab", "A", "B"), link("ba", "B", "A")));
        SchedulingChanges removed = new SchedulingChanges();
        removed.tasks.remove.add(Id.str("B"));

        List<Candidate> candidates = List.of(
            candidate("missing", reassign("nope", "dev2")),
            candidate("cycle", cycle),
            candidate("removed", removed));
        EvaluateOptions options = new EvaluateOptions()
            .withLevel(new LevelingOptions())
            .withFocus(List.of(Id.str("B")));
        EvaluateResult result = project.evaluate(candidates, options);

        for (CandidateResult candidate : result.candidates) {
            if (candidate.error != null) {
                System.out.println(candidate.id + " rejected at "
                    + candidate.error.path + ": " + candidate.error.message);
                continue;
            }
            Metrics scheduled = candidate.scheduled;
            if (scheduled.cycles.value > 0) {
                System.out.println(candidate.id + " is cyclic, leveled: " + (candidate.leveled != null));
            }
            for (TaskFinish task : scheduled.finish.tasks) {
                if (task.finish == null) {
                    System.out.println(candidate.id + " removes focus task " + task.id);
                }
            }
            System.out.println(candidate.id + " cost " + candidate.cost);
        }
    }
}
```

```text
missing rejected at assignments.update[0]: unknown assignment id
cycle is cyclic, leveled: false
cycle cost 1002.0
removed removes focus task B
removed cost 0.0
```

Rank only candidates that have a `cost`. The cyclic candidate has one, but the penalty keeps it at the bottom. Its 1002 is the cycle penalty plus the two resource-days of overload `dev1` still has.

Call-level errors still throw. An unknown focus id fails the whole call with an [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>) at path `focus[i]`, and a stale token with a [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/project-exception.md>) with code `STALE_BASELINE`. Both are unchecked. See [Handling errors](<https://docs.dhtmlx.com/planning-engine/java/error-handling.md>).

### Parallel evaluation

Each candidate runs on its own copy, so candidates are independent. [`Project#evaluateWith`](<https://docs.dhtmlx.com/planning-engine/java/api/project-evaluate.md#evaluatewith>) takes an [`Execution`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/execution.md>) and can spread them over threads:

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

public class ParallelEvaluate {
    static TaskData task(String id, Moment start, double duration) {
        return new TaskData(Id.str(id)).withStartDate(start).withDuration(duration);
    }

    static AssignmentData assign(String id, String task, String resource) {
        return new AssignmentData()
            .withId(Id.str(id))
            .withTask(Id.str(task))
            .withResource(Id.str(resource))
            .withValue(1.0);
    }

    /** Tasks A and B, two days each, both on dev1 in the same week. */
    static Project solvedProject() {
        Moment monday = Moment.fromYmdHm(2026, 8, 10, 8, 0);
        ProjectData data = new ProjectData()
            .withTasks(List.of(task("A", monday, 2.0), task("B", monday, 2.0)))
            .withResources(List.of(
                new ResourceData().withId(Id.str("dev1")),
                new ResourceData().withId(Id.str("dev2"))))
            .withAssignments(List.of(assign("aA", "A", "dev1"), assign("aB", "B", "dev1")));
        Project project = new Project();
        project.solve(data, new SolveOptions());
        return project;
    }

    static SchedulingChanges reassign(String assignment, String resource) {
        SchedulingChanges changes = new SchedulingChanges();
        changes.assignments.update.add(
            new AssignmentUpdate(Id.str(assignment)).withResource(Patch.of(Id.str(resource))));
        return changes;
    }

    static Candidate candidate(String id, SchedulingChanges changes) {
        return new Candidate().withId(Id.str(id)).withChanges(changes);
    }

    public static void main(String[] args) throws Exception {
        Project project = solvedProject();
        // One candidate per resource that could take task B.
        List<Candidate> candidates = List.of("dev1", "dev2").stream()
            .map(dev -> candidate(dev, reassign("aB", dev)))
            .toList();
        EvaluateOptions options = new EvaluateOptions();

        EvaluateResult result = project.evaluateWith(candidates, options, Execution.parallel(4));

        // Same results, same order as project.evaluate(candidates, options).
        for (CandidateResult candidate : result.candidates) {
            System.out.println(candidate.id + ": cost " + candidate.cost);
        }
    }
}
```

[`Execution.parallel()`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/execution.md#parallel>) (or `Execution.parallel(null)`) uses `Runtime.availableProcessors()` threads. `Execution.parallel(0)` or a negative count throws an `IllegalArgumentException`. [`Execution.sequential()`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/execution.md#sequential>) is what [`evaluate`](<https://docs.dhtmlx.com/planning-engine/java/api/project-evaluate.md>) does.

The call runs the candidates on a private fixed thread pool of `min(threads, candidates)` workers and shuts it down before returning. It blocks the calling thread until every candidate is done. Peak memory is one candidate copy per worker.

The results are identical to the sequential run and keep input order. If the call fails, you get the same exception as in a sequential run: the first one in input order. Later candidates may already have run.

`evaluate` and `evaluateWith` are synchronous and CPU-bound, like every call in the library. In a reactive server or on a request thread, run large evaluations on a worker executor. Don't call `evaluateWith` on the same `Project` from several threads at once: the project isn't synchronized. See the [`Execution` reference](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/execution.md>) and [Call from async code](<https://docs.dhtmlx.com/planning-engine/java/how-to/call-from-async-code.md>).

### Committing the winner

[`evaluate`](<https://docs.dhtmlx.com/planning-engine/java/api/project-evaluate.md>) never changes the project. To commit the chosen candidate, run its changes for real: [`apply`](<https://docs.dhtmlx.com/planning-engine/java/api/project-apply.md>) the same change set, then [`level`](<https://docs.dhtmlx.com/planning-engine/java/api/project-level.md>) with the same [`LevelingOptions`](<https://docs.dhtmlx.com/planning-engine/java/api/resources/leveling-options.md>), `step` included. With other options, the committed plan isn't the one that was scored. If you scored without `level`, follow `apply` with [`solve(null, …)`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) instead.

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

public class CommitWinner {
    static TaskData task(String id, Moment start, double duration) {
        return new TaskData(Id.str(id)).withStartDate(start).withDuration(duration);
    }

    static AssignmentData assign(String id, String task, String resource) {
        return new AssignmentData()
            .withId(Id.str(id))
            .withTask(Id.str(task))
            .withResource(Id.str(resource))
            .withValue(1.0);
    }

    /** Tasks A and B, two days each, both on dev1 in the same week. */
    static Project solvedProject() {
        Moment monday = Moment.fromYmdHm(2026, 8, 10, 8, 0);
        ProjectData data = new ProjectData()
            .withTasks(List.of(task("A", monday, 2.0), task("B", monday, 2.0)))
            .withResources(List.of(
                new ResourceData().withId(Id.str("dev1")),
                new ResourceData().withId(Id.str("dev2"))))
            .withAssignments(List.of(assign("aA", "A", "dev1"), assign("aB", "B", "dev1")));
        Project project = new Project();
        project.solve(data, new SolveOptions());
        return project;
    }

    static SchedulingChanges reassign(String assignment, String resource) {
        SchedulingChanges changes = new SchedulingChanges();
        changes.assignments.update.add(
            new AssignmentUpdate(Id.str(assignment)).withResource(Patch.of(Id.str(resource))));
        return changes;
    }

    static Candidate candidate(String id, SchedulingChanges changes) {
        return new Candidate().withId(Id.str(id)).withChanges(changes);
    }

    public static void main(String[] args) throws Exception {
        Project project = solvedProject();
        List<Candidate> candidates = List.of(
            candidate("stay", new SchedulingChanges()),
            candidate("dev2", reassign("aB", "dev2")));
        LevelingOptions leveling = new LevelingOptions();
        EvaluateOptions options = new EvaluateOptions().withLevel(leveling);
        EvaluateResult result = project.evaluate(candidates, options);

        // Lowest cost wins; rejected candidates have no cost.
        Candidate winner = null;
        double best = Double.POSITIVE_INFINITY;
        for (int i = 0; i < candidates.size(); i++) {
            Double cost = result.candidates.get(i).cost;
            if (cost != null && cost < best) {
                best = cost;
                winner = candidates.get(i);
            }
        }

        if (winner != null) {
            SchedulingChanges changes = winner.changes.deepClone();
            // Throws STALE_BASELINE if the project changed since the evaluation.
            changes.baselineId = result.baselineId;
            ApplyReport report = project.apply(changes);
            if (report.rejected.isEmpty()) {
                // The same options that scored the candidate.
                project.level(leveling);
                System.out.println("committed " + winner.id + " (cost " + best + ")");
            }
        }
    }
}
```

Results come back in input order, so index `i` in `result.candidates` pairs with index `i` in your list. The code copies the winning change set with [`deepClone()`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/scheduling-changes.md#deepclone>) before setting its `baselineId`, so the candidate stays as it was. With the token from the result, `apply` throws a `ProjectException` with code `STALE_BASELINE` when another edit landed in between. In that case, evaluate again.

If you scored with [`EvaluateOptions.clearFirst`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/evaluate-options.md#clearfirst>), commit with `level` and [`LevelingOptions.clearFirst`](<https://docs.dhtmlx.com/planning-engine/java/api/resources/leveling-options.md#clearfirst>) left at `true` (the default), so the commit re-levels from zero like the scoring did.

To handle a rejected `apply`, see [Recover from rejected changes](<https://docs.dhtmlx.com/planning-engine/java/how-to/recover-from-rejected-changes.md>). To generate candidates automatically instead of writing them by hand, use [`suggestAssignments`](<https://docs.dhtmlx.com/planning-engine/java/api/project-suggest-assignments.md>), which scores its steps the same way. See [Suggesting assignments](<https://docs.dhtmlx.com/planning-engine/java/resources-and-assignment/suggesting-assignments.md>).
