Import from dhtmlxGantt#

Load a dhtmlxGantt project into Planning Engine and get the same task dates the gantt auto-scheduler would give you.

Planning Engine's scheduling step is a port of the dhtmlxGantt v2 auto-scheduling engine, and its records follow gantt's fields: startDate is gantt's start_date, constraintType is constraint_type, autoScheduling is auto_scheduling. Most of the import copies fields one to one. The steps below cover the places where a straight copy gives different dates or fails validation.

Prerequisites#

  • Your gantt tasks, links, resources and assignments are parsed into your own Java records. The example uses Java records that mirror gantt's JSON fields.
  • You know each task's effective calendar, as gantt resolves it.

To read gantt-named JSON straight into ProjectData, see Exchange JSON with a client. The steps below apply to the values either way.

Steps#

  1. Set the scheduling config to gantt's model. Use CalendarMode.TASK for task calendars only, or CalendarMode.TASK_INHERITED if gantt has inherit_calendar: true. Copy gantt's work_time into useCalendars explicitly. It defaults to true, so Planning Engine schedules on calendars unless you turn it off:

    boolean ganttWorkTime = true; // gantt `config.work_time`
    
    var config = new SchedulingConfig()
        .withCalendarMode(CalendarMode.TASK)
        .withUseCalendars(ganttWorkTime);
    

    Set rootId only when gantt's virtual root isn't 0. The other auto-scheduling options (applyConstraints, gapBehavior, moveProjects, scheduleFromEnd, projectStart, useProgress) are gantt's options of the same name, in camelCase.

  2. Add one CalendarData per calendar your tasks use, and set each task's calendarId to its effective calendar. gantt picks that calendar in its adapter: the task's own, else its resource's, else a calendar merged from all its resources, else the parent's. Send the result, not the rules. A merged resource calendar goes in once, under its synthetic id. Don't repeat gantt's precedence or merging in your import code.

  3. Map each task to a TaskData:

    • gantt type: "project" becomes TaskType.SUMMARY. A summary takes both startDate and endDate, or neither. Drop its duration, or strict validation rejects the task.
    • A parent equal to rootId means "no parent". You can copy it as is.
    • A leaf task needs two of startDate, endDate and duration. If you send all three, they must agree.
    • If your gantt event handlers cancel auto-scheduling for a task, set autoScheduling to false on it. The planner never moves it.
  4. Map gantt link type values "0", "1", "2" and "3" to LinkType.FS, SS, FF and SF. Copy lag and leave lagUnit as null, because gantt measures lag in config.duration_unit.

  5. Convert each assignment to a slice of its task's work in AssignmentData. Don't send gantt's mode.

    • default and fixedDuration: delay becomes offset and duration stays duration. Leave both null to book the whole task.
    • fixedDates: set offset and duration to the worked time its dates cover, measured from the task start.

    A slice ends at the task end. gantt's fixedDuration can run past it.

  6. Solve the data with Project#solve and write the changed fields from result.updates back into gantt. The updates include summaries, which the gantt JS adapter leaves to gantt core. Apply those too, or your summary spans go stale.

The full example below imports a summary with two tasks, a milestone, two links, a part-time calendar and one default-mode assignment:

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

public class ImportFromGantt {
  // Records as your server holds them after parsing gantt's JSON.
  record GanttTask(
      int id,
      int parent,
      String type, // gantt `type`
      String startDate,
      String endDate, // null when gantt has none
      double duration,
      String calendarId) {} // effective calendar, already resolved by gantt

  record GanttLink(int id, int source, int target, String type, double lag) {} // `type`: "0".."3"

  record GanttAssignment(
      int id,
      int taskId,
      String resourceId,
      double value,
      Double delay, // `default` and `fixedDuration` modes
      Double duration) {} // `default` and `fixedDuration` modes

  static LinkType linkType(String type) {
    return switch (type) {
      case "0" -> LinkType.FS;
      case "1" -> LinkType.SS;
      case "2" -> LinkType.FF;
      case "3" -> LinkType.SF;
      default -> throw new IllegalArgumentException("unknown link type " + type);
    };
  }

  static TaskData task(GanttTask g) {
    // Moment.parse reads "YYYY-MM-DD HH:MM". Convert other gantt `date_format` output first.
    var t = new TaskData(Id.num(g.id()))
        .withParent(Id.num(g.parent())) // gantt's root (0) means "no parent"
        .withStartDate(Moment.parse(g.startDate()))
        .withCalendarId(Id.str(g.calendarId()));
    switch (g.type()) {
      case "project" -> {
        // A summary takes both dates or neither, never a duration.
        t.taskType = TaskType.SUMMARY;
        if (g.endDate() != null) {
          t.endDate = Moment.parse(g.endDate());
        } else {
          t.startDate = null;
        }
      }
      case "milestone" -> {
        t.taskType = TaskType.MILESTONE;
        t.duration = 0.0;
      }
      default -> t.duration = g.duration();
    }
    return t;
  }

  public static void main(String[] args) throws Exception {
    var ganttTasks = List.of(
        new GanttTask(1, 0, "project", "2026-03-02 00:00", "2026-03-07 00:00", 5, "global"),
        new GanttTask(2, 1, "task", "2026-03-02 00:00", null, 3, "global"),
        new GanttTask(3, 1, "task", "2026-03-02 00:00", null, 4, "part-time"),
        new GanttTask(4, 0, "milestone", "2026-03-02 00:00", null, 0, "global"));
    var ganttLinks = List.of(
        new GanttLink(1, 2, 3, "0", 0),
        new GanttLink(2, 1, 4, "0", 0));
    var ganttAssignments = List.of(new GanttAssignment(1, 3, "dev", 1.0, 1.0, 2.0));

    var config = new SchedulingConfig()
        .withCalendarMode(CalendarMode.TASK) // gantt's calendar model
        .withUseCalendars(true); // gantt `config.work_time`

    var calendars = List.of(
        new CalendarData().withId(Id.str("global")),
        new CalendarData()
            .withId(Id.str("part-time"))
            .withWorktime(new Worktime()
                // Index 0 is Sunday: Mon-Thu.
                .withDays(new boolean[] {false, true, true, true, true, false, false})));

    var tasks = new ArrayList<TaskData>();
    for (var g : ganttTasks) tasks.add(task(g));

    var links = new ArrayList<LinkData>();
    for (var g : ganttLinks) {
      links.add(new LinkData()
          .withId(Id.num(g.id()))
          .withSource(Id.num(g.source()))
          .withTarget(Id.num(g.target()))
          .withLinkType(linkType(g.type()))
          .withLag(g.lag())); // lagUnit stays null: gantt lag is in `config.duration_unit`
    }

    var assignments = new ArrayList<AssignmentData>();
    for (var g : ganttAssignments) {
      assignments.add(new AssignmentData()
          .withId(Id.num(g.id()))
          .withTask(Id.num(g.taskId()))
          .withResource(Id.str(g.resourceId()))
          .withValue(g.value())
          .withOffset(g.delay())
          .withDuration(g.duration()));
    }

    var data = new ProjectData()
        .withConfig(config)
        .withCalendars(calendars)
        .withTasks(tasks)
        .withLinks(links)
        .withResources(List.of(new ResourceData().withId(Id.str("dev"))))
        .withAssignments(assignments);

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

    for (var m : result.updates) {
      // Only changed fields are set. Summaries are included: write them back too.
      if (m.startDate != null) System.out.println(m.id + " start_date = " + m.startDate);
      if (m.endDate != null) System.out.println(m.id + " end_date = " + m.endDate);
    }
  }
}

Keep gantt's triggers on your side#

Planning Engine has no UI triggers and no events. Your code decides when to schedule.

  • Call apply and then solve(null, ...) wherever gantt would run auto-scheduling: after the user adds a link, drags a task, or saves the lightbox. See Editing a project.
  • gantt.autoSchedule(id, inclusive) is SolveOptions.subtree. See Reschedule a branch.
  • Change sets can't add or edit calendars. If an assignment change gives a task a new merged calendar, send the whole project again with solve(data, ...).
  • gantt core rewrites descendant dates when inherited calendars or effort change. Forward those date and duration edits as ordinary TaskUpdate changes. Planning Engine doesn't recreate gantt's effort rules.

Differences to expect#

  • Strict validation rejects input that gantt coerces: duplicate ids, a dated constraint with no constraintDate, a leaf task with fewer than two of start, end and duration, a dateless summary with no dated children. The rejection is an InputException with the field path. To get gantt's coercions with a report of each fix, see Accept messy input.
  • TimeUnit has no week, month or year, and calendars with customWeeks are rejected. A gantt merged calendar can contain customWeeks, so such projects can't be imported yet.
  • A Moment is a naive local date-time with no timezone and no DST. gantt's JS engine uses the host time zone, so its arithmetic follows DST. Convert dates to local wall-clock time before you build the data.
  • result.notes lists the links, tasks and assignments the engine dropped. gantt drops them silently.
  • Three cases give different dates on purpose. With projectConstraint = true, children inherit the parent's constraint. gantt's engine ignores it. With useProgress, a completed child of a moving summary stays in place. gantt drags it along. With descendantLinks = true in block mode, gantt fails with a cycle error and Planning Engine schedules the project. See Links, constraints and summaries.
  • Summary-to-summary links in block mode, combined with leaf links between the same summaries, can give different dates. On long chains of such links the dates grow without bound. See Error handling.

Result#

The example prints the changed fields:

3 start_date = 2026-03-05 00:00
3 end_date = 2026-03-12 00:00
1 end_date = 2026-03-12 00:00
4 start_date = 2026-03-12 00:00
4 end_date = 2026-03-12 00:00

Task 3 starts when task 2 finishes and works Monday to Thursday only, so it ends a week later. The summary 1 grows to cover it. Task 2 didn't move, so it isn't listed. To get every date on the first load, see Load all task dates.