[Canonical page](<https://docs.dhtmlx.com/planning-engine/java/quick-start/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Quick start

By the end of this page, you'll have a Java program that schedules two linked tasks, and a live project that reschedules them when one changes.

## Requirements

- JDK 21 or newer. The library is a single JAR with no dependencies of its own.
- No async setup. Every call is synchronous and CPU-only, with no I/O and no clock.

## Install

The library comes from a private Maven registry at `git.dhtmlx.com`. With your license or trial you received a username and a password for one of two editions:

| Edition | Registry URL | Coordinates |
| --- | --- | --- |
| Full | `https://git.dhtmlx.com/api/packages/plen/maven` | `com.dhtmlx:plen` |
| Trial | `https://git.dhtmlx.com/api/packages/plen-trial/maven` | `com.dhtmlx:plen-trial` |

The trial is the full engine with one limit: a project with more than 50 tasks is rejected. Only the artifact id differs. The package `org.dhtmlx.plen` and its classes are the same, so the code in these docs runs on both.

The examples below use the full edition. For the trial, replace `plen` with `plen-trial` in the registry URL and in the artifact id.

### Maven

Add the credentials to `~/.m2/settings.xml`, not to the project. They come from environment variables:

```xml
<settings>
  <servers>
    <server>
      <id>dhtmlx</id>
      <username>${env.DHTMLX_MAVEN_USER}</username>
      <password>${env.DHTMLX_MAVEN_PASSWORD}</password>
    </server>
  </servers>
</settings>
```

In `pom.xml`, add the repository and the dependency. The repository `id` must match the server `id` in `settings.xml`:

```xml
<repositories>
  <repository>
    <id>dhtmlx</id>
    <url>https://git.dhtmlx.com/api/packages/plen/maven</url>
  </repository>
</repositories>

<dependencies>
  <dependency>
    <groupId>com.dhtmlx</groupId>
    <artifactId>plen</artifactId>
    <version>1.0.0</version>
  </dependency>
</dependencies>
```

Set the variables, then resolve the dependency:

```bash
# bash / zsh
export DHTMLX_MAVEN_USER=your-username
export DHTMLX_MAVEN_PASSWORD=your-password
mvn dependency:resolve
```

```powershell
# PowerShell
$env:DHTMLX_MAVEN_USER = "your-username"
$env:DHTMLX_MAVEN_PASSWORD = "your-password"
```

On a CI server, set the same two variables as secrets.

### Gradle

In `settings.gradle.kts`, add the registry:

```kotlin
dependencyResolutionManagement {
    repositories {
        mavenCentral()
        maven {
            name = "dhtmlx"
            url = uri("https://git.dhtmlx.com/api/packages/plen/maven")
            credentials(PasswordCredentials::class)
            // Optional: take com.dhtmlx only from this registry.
            content { includeGroup("com.dhtmlx") }
        }
    }
}
```

`credentials(PasswordCredentials::class)` reads the `dhtmlxUsername` and `dhtmlxPassword` properties, named after the repository. Put them in `~/.gradle/gradle.properties`:

```properties
dhtmlxUsername=your-username
dhtmlxPassword=your-password
```

Or set them as the environment variables `ORG_GRADLE_PROJECT_dhtmlxUsername` and `ORG_GRADLE_PROJECT_dhtmlxPassword`, for example on CI. Then add the dependency in `build.gradle.kts`:

```kotlin
dependencies {
    implementation("com.dhtmlx:plen:1.0.0")
}
```

### Credentials and IDEs

IntelliJ IDEA and Eclipse import the Maven or Gradle setup above. IntelliJ IDEA reads `~/.m2/settings.xml`. If it uses its bundled Maven with another settings file, set the file under *Settings → Build, Execution, Deployment → Build Tools → Maven → User settings file*. The IDE process must see the environment variables. On macOS, start the IDE from a shell, or put the credentials directly into `settings.xml` or `gradle.properties`.

### Move from the trial to the full edition

Change the registry URL from `.../plen-trial/maven` to `.../plen/maven`, use your full-edition credentials, and change the artifact id from `plen-trial` to `plen`. Keep the version. The code doesn't change.

### If the build can't resolve the library

- **`401 Unauthorized`:** the username or password is wrong, or the variables aren't set in the shell that runs the build. With Maven, check that the server `id` in `settings.xml` matches the repository `id` in `pom.xml`.
- **`Could not find com.dhtmlx:plen`:** the registry and the artifact don't match. Trial credentials see only `plen-trial`. Also check that the URL ends in `/maven`.
- **A failed download is cached:** Maven and Gradle remember failed resolutions. Run `mvn -U ...`, or `gradle --refresh-dependencies`.

### Imports

Everything lives in one package, `org.dhtmlx.plen`, so `import org.dhtmlx.plen.*;` brings in the engine, the skill layer and the JSON API. A few names in that package clash with JDK types: [`MatchException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/match-exception.md>) and `Runtime` with `java.lang`, `Set` and `Queue` with `java.util`. The examples on this page never use those names, so the wildcard import is fine. If you also write `import java.util.*;`, import the clashing names you need one by one.

## Schedule a snapshot

Start with the smallest useful plan: two tasks and a finish-to-start link. Both tasks start on Monday, 2026-08-10, at 08:00, so task 2 starts too early. It has to wait for task 1 to finish.

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

public class QuickStart {
    public static void main(String[] args) {
        Moment monday = Moment.fromYmdHm(2026, 8, 10, 8, 0);
        if (monday == null) {
            throw new IllegalArgumentException("invalid date");
        }

        ProjectData data = new ProjectData()
            .withTasks(List.of(
                new TaskData(Id.num(1)).withStartDate(monday).withDuration(2.0),
                new TaskData(Id.num(2)).withStartDate(monday).withDuration(1.0)))
            .withLinks(List.of(
                new LinkData()
                    .withId(Id.num(1))
                    .withSource(Id.num(1))
                    .withTarget(Id.num(2))
                    .withLinkType(LinkType.FS)));

        try {
            ScheduleResult result = Engine.schedule(data, new SolveOptions());
            for (Mutation update : result.updates) {
                System.out.println("task " + update.id + ": " + update.startDate + " -> " + update.endDate);
            }
        } catch (InputException e) {
            System.err.println("invalid input at " + e.path + ": " + e.getMessage());
            System.exit(1);
        }
    }
}
```

Run it from your build, or straight from the source file with the JAR on the class path:

```bash
java -cp plen.jar QuickStart.java
```

Output:

```
task 2: 2026-08-12 08:00 -> 2026-08-13 08:00
```

[`Engine.schedule`](<https://docs.dhtmlx.com/planning-engine/java/api/engine-schedule.md>) takes a [`ProjectData`](<https://docs.dhtmlx.com/planning-engine/java/api/data/project-data.md>) snapshot and returns a [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md>). Its [`updates`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#updates>) list holds only the tasks that moved, as [`Mutation`](<https://docs.dhtmlx.com/planning-engine/java/api/results/mutation.md>) records. Task 1 didn't move, so it's not in the list.

Durations are working time in days. The default calendar works Monday to Friday, 08:00-12:00 and 13:00-17:00. That's why the two-day task 1 ends on Wednesday at 08:00, not on Tuesday evening.

Records such as [`TaskData`](<https://docs.dhtmlx.com/planning-engine/java/api/data/task-data.md>) and [`LinkData`](<https://docs.dhtmlx.com/planning-engine/java/api/data/link-data.md>) are mutable classes with public fields and a fluent `withX(...)` setter per field. List setters take any `Collection`, so `List.of(...)` works. Dates are [`Moment`](<https://docs.dhtmlx.com/planning-engine/java/api/data/moment.md>) values: local time, minute precision, no time zone. [`Moment.fromYmdHm`](<https://docs.dhtmlx.com/planning-engine/java/api/data/moment.md#fromymdhm>) returns `null` for an invalid date, so the example checks for it before using the value. Ids are [`Id`](<https://docs.dhtmlx.com/planning-engine/java/api/data/id.md>) values, built with `Id.num(1)` or `Id.str("a")`. Note that `Id.num(1)` and `Id.str("1")` name the same task. [Records, ids and dates](<https://docs.dhtmlx.com/planning-engine/java/records-ids-and-dates.md>) covers these types in detail.

### When the input is invalid

Every leaf task needs two of `startDate`, `endDate` and `duration`. Remove task 2's start date and keep only its duration:

```java
new TaskData(Id.num(2)).withDuration(1.0)
```

[`Engine.schedule`](<https://docs.dhtmlx.com/planning-engine/java/api/engine-schedule.md>) now rejects the snapshot before it schedules anything, and the `catch` block prints:

```
invalid input at tasks[1].start_date: task needs two of start_date / end_date / duration
```

The [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>) has a [`path`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md#path>) field that points to the field that failed. The path uses the JSON field names, so `startDate` shows up as `start_date`. Add the start date back and the program runs again.

`InputException` is unchecked, like every exception the library throws, so the compiler won't remind you to catch it. Bad input is the only thing that makes this call throw. Dependency cycles, constraint conflicts and dropped links come back as data in [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md>) ([`cycles`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#cycles>), [`conflicts`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#conflicts>), [`notes`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#notes>)), and the call still returns normally.

## Keep a live project

[`Engine.schedule`](<https://docs.dhtmlx.com/planning-engine/java/api/engine-schedule.md>) fits batch jobs and request handlers that see each snapshot once. An editor backend works differently. It loads the plan once, then sends small changes as the user works. That's what [`Project`](<https://docs.dhtmlx.com/planning-engine/java/api/project/project.md>) does.

This program loads the same two tasks, previews a change, then commits it:

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

public class LiveProject {
    public static void main(String[] args) {
        Moment monday = Moment.fromYmdHm(2026, 8, 10, 8, 0);
        if (monday == null) {
            throw new IllegalArgumentException("invalid date");
        }

        ProjectData data = new ProjectData()
            .withTasks(List.of(
                new TaskData(Id.num(1)).withStartDate(monday).withDuration(2.0),
                new TaskData(Id.num(2)).withStartDate(monday).withDuration(1.0)))
            .withLinks(List.of(
                new LinkData()
                    .withId(Id.num(1))
                    .withSource(Id.num(1))
                    .withTarget(Id.num(2))
                    .withLinkType(LinkType.FS)));

        Project project = new Project();
        ScheduleResult first = project.solve(data, new SolveOptions());
        System.out.println("loaded, " + first.updates.size() + " task(s) moved");

        // The user stretches task 1 to three days.
        SchedulingChanges changes = new SchedulingChanges();
        changes.tasks.update.add(new TaskUpdate(Id.num(1)).withDuration(Patch.of(3.0)));

        ScheduleResult preview = project.preview(changes, new PreviewOptions());
        System.out.println("preview: " + preview.updates.size() + " task(s) would move");

        ApplyReport report = project.apply(changes);
        if (report.needsResync) {
            System.err.println("an edit was rejected; reload the project with solve(data, ...)");
            System.exit(1);
        }

        ScheduleResult result = project.solve(null, new SolveOptions());
        for (Mutation update : result.updates) {
            System.out.println("task " + update.id + ": " + update.startDate + " -> " + update.endDate);
        }
    }
}
```

Output:

```
loaded, 1 task(s) moved
preview: 2 task(s) would move
task 1: 2026-08-10 08:00 -> 2026-08-13 08:00
task 2: 2026-08-13 08:00 -> 2026-08-14 08:00
```

Each call does one job:

- [`Project#solve`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) with `data` loads and schedules the snapshot. It works on its own deep copy of `data`, so later changes to your object don't reach the project. The scheduled dates become the project's baseline.
- [`Project#preview`](<https://docs.dhtmlx.com/planning-engine/java/api/project-preview.md>) runs the change on a copy of the baseline. The project doesn't change, so it's safe to call on every mouse move during a drag.
- [`Project#apply`](<https://docs.dhtmlx.com/planning-engine/java/api/project-apply.md>) commits the change and marks the project as unsolved.
- [`solve(null, ...)`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) reschedules the current state. Task 1 is reported because it was edited, and task 2 moves to follow it.

Edits are [`SchedulingChanges`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/scheduling-changes.md>). A [`TaskUpdate`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/task-update.md>) sets only the fields you give it. Each field is a [`Patch<T>`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/patch.md>): [`Patch.of(value)`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/patch.md#of>) sets it, [`Patch.nullValue()`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/patch.md#nullvalue>) clears it, and [`Patch.missing()`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/patch.md#missing>) (the default) leaves it alone.

A `Project` isn't `AutoCloseable`, so there's nothing to close when you're done with it. It's an ordinary object that the garbage collector cleans up. It also isn't synchronized: use one instance from one thread at a time.

### When an edit is rejected

[`apply`](<https://docs.dhtmlx.com/planning-engine/java/api/project-apply.md>) accepts what it can and reports the rest in the returned [`ApplyReport`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/apply-report.md>). It doesn't throw for a bad operation. To see this, remove a task that doesn't exist. Add this line before the `apply` call:

```java
changes.tasks.remove.add(Id.num(99));
```

The report's [`rejected`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/apply-report.md#rejected>) list now has an entry with this `path` and `message`:

```
tasks.remove[0] unknown task id
```

[`needsResync`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/apply-report.md#needsresync>) is now `true`. Your copy of the plan and the project's copy no longer match. The next [`solve(null, ...)`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) throws a [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/project-exception.md>) with `code == ProjectError.NEEDS_RESYNC` (message `needs-resync`). To fix it, send the full data again with `solve(data, ...)`. That's why the example checks `needsResync` before it solves. [Recover from rejected changes](<https://docs.dhtmlx.com/planning-engine/java/how-to/recover-from-rejected-changes.md>) covers this flow in detail.

## Next steps

- [Records, ids and dates](<https://docs.dhtmlx.com/planning-engine/java/records-ids-and-dates.md>): building records, [`Id`](<https://docs.dhtmlx.com/planning-engine/java/api/data/id.md>), [`Moment`](<https://docs.dhtmlx.com/planning-engine/java/api/data/moment.md>), [`Patch<T>`](<https://docs.dhtmlx.com/planning-engine/java/api/changes/patch.md>) and read-only results.
- [Project data](<https://docs.dhtmlx.com/planning-engine/java/scheduling/project-data.md>): tasks, links, calendars, resources and assignments.
- [Scheduling a project](<https://docs.dhtmlx.com/planning-engine/java/scheduling/scheduling-a-project.md>): one-shot and stateful scheduling, options and subtrees.
- [Reading results](<https://docs.dhtmlx.com/planning-engine/java/scheduling/reading-results.md>): updates, conflicts, cycles and notes.
- [Working-time calendars](<https://docs.dhtmlx.com/planning-engine/java/scheduling/working-time-calendars.md>): hours, weekdays and date exceptions.
- [Editing a project](<https://docs.dhtmlx.com/planning-engine/java/live-projects/editing-a-project.md>): [`apply`](<https://docs.dhtmlx.com/planning-engine/java/api/project-apply.md>), baselines and resync.
- [Previewing changes](<https://docs.dhtmlx.com/planning-engine/java/live-projects/previewing-changes.md>): what-if results without committing.
- [Leveling resources](<https://docs.dhtmlx.com/planning-engine/java/resources-and-assignment/leveling-resources.md>): remove resource overallocation.
- [Error handling](<https://docs.dhtmlx.com/planning-engine/java/error-handling.md>): [`InputException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/input-exception.md>), [`ProjectException`](<https://docs.dhtmlx.com/planning-engine/java/api/errors/project-exception.md>) and what comes back as data instead.
- [Exchange JSON with a client](<https://docs.dhtmlx.com/planning-engine/java/how-to/exchange-json-with-a-client.md>): the JSON API for HTTP endpoints that pass JSON payloads through.
- [Call from async code](<https://docs.dhtmlx.com/planning-engine/java/how-to/call-from-async-code.md>): run long calls off the request thread.
- [Share a project between threads](<https://docs.dhtmlx.com/planning-engine/java/how-to/share-a-project-between-threads.md>): copies and locking.
- [API reference](<https://docs.dhtmlx.com/planning-engine/java/api/overview.md>): every public type and method.
