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 input uses gantt's field names (start_date, duration, parent, constraint_type, auto_scheduling). It also accepts several gantt forms as is: task type: "project", link type values "0" to "3", and assignment task_id / resource_id. Unknown fields such as text are ignored. 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 plain objects with gantt's field names.
  • You know each task's effective calendar, as gantt resolves it.

Steps#

  1. Set the scheduling config to gantt's model. Use calendar_mode: "task" for task calendars only, or "task-inherited" if gantt has inherit_calendar: true. Copy gantt's work_time into use_calendars explicitly. If you leave use_calendars out, Planning Engine schedules on calendars:

    const ganttWorkTime = true; // gantt `config.work_time`
    
    const config = {
      calendar_mode: "task",
      use_calendars: ganttWorkTime,
    };
    

    Set root_id only when gantt's virtual root isn't 0. The other auto-scheduling options (apply_constraints, gap_behavior, move_projects, schedule_from_end, project_start, use_progress) keep their gantt names.

  2. Add one calendar per calendar your tasks use, and set each task's calendar_id 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 task record:

    • Keep gantt's type: "project". Planning Engine reads it as a summary. A summary takes both start_date and end_date, or neither. Drop its duration, or strict validation rejects the task.
    • A parent equal to root_id means "no parent". You can copy it as is.
    • A leaf task needs two of start_date, end_date and duration. If you send all three, they must agree.
    • If your gantt event handlers cancel auto-scheduling for a task, set auto_scheduling: false on it. The planner never moves it.
  4. Copy gantt links as they are. Link type values "0", "1", "2" and "3" mean "fs", "ss", "ff" and "sf". Copy lag and leave lag_unit unset, because gantt measures lag in config.duration_unit.

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

    • default and fixedDuration: delay becomes offset and duration stays duration. Leave both unset 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 imports a summary with two tasks, a milestone, two links, a part-time calendar and one default-mode assignment:

import { Project } from "plen-js";

// gantt data as your app holds it: gantt's field names, dates as strings.
const gantt = {
  tasks: [
    { id: 1, parent: 0, type: "project", text: "Release", start_date: "2026-03-02 00:00", end_date: "2026-03-07 00:00", duration: 5, calendar_id: "global" },
    { id: 2, parent: 1, type: "task", text: "Build", start_date: "2026-03-02 00:00", duration: 3, calendar_id: "global" },
    { id: 3, parent: 1, type: "task", text: "Test", start_date: "2026-03-02 00:00", duration: 4, calendar_id: "part-time" },
    { id: 4, parent: 0, type: "milestone", text: "Ship", start_date: "2026-03-02 00:00", duration: 0, calendar_id: "global" },
  ],
  links: [
    { id: 1, source: 2, target: 3, type: "0", lag: 0 },
    { id: 2, source: 1, target: 4, type: "0", lag: 0 },
  ],
  resources: [{ id: "dev" }],
  assignments: [
    { id: 1, task_id: 3, resource_id: "dev", value: 1, mode: "default", delay: 1, duration: 2 },
  ],
};

function toTask(t) {
  const task = {
    id: t.id,
    parent: t.parent,
    type: t.type,
    start_date: t.start_date,
    calendar_id: t.calendar_id,
  };
  if (t.type === "project") {
    // A summary takes both dates or neither, never a duration.
    if (t.end_date) task.end_date = t.end_date;
    else delete task.start_date;
  } else {
    task.duration = t.duration;
  }
  return task;
}

const data = {
  config: {
    calendar_mode: "task", // gantt's calendar model
    use_calendars: true, // gantt `config.work_time`
  },
  calendars: [
    { id: "global" },
    // Mon-Thu, as gantt resolved it for task 3.
    { id: "part-time", worktime: { days: [0, 1, 1, 1, 1, 0, 0] } },
  ],
  tasks: gantt.tasks.map(toTask),
  links: gantt.links, // link types "0".."3" and lag are accepted as is
  resources: gantt.resources.map(({ id }) => ({ id })),
  // `default` and `fixedDuration` modes: `delay` becomes `offset`, no `mode`.
  assignments: gantt.assignments.map(({ id, task_id, resource_id, value, delay, duration }) => ({
    id,
    task_id,
    resource_id,
    value,
    offset: delay,
    duration,
  })),
};

const project = new Project();
const result = project.solve(data);

// Only changed fields are set. Summaries are included: write them back too.
for (const update of result.updates) {
  if (update.start_date) console.log(`${update.id} start_date = ${update.start_date}`);
  if (update.end_date) console.log(`${update.id} end_date = ${update.end_date}`);
}

Keep gantt's triggers on your side#

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

  • Call project.apply and then project.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 the subtree solve option. 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 project.solve(data).
  • gantt core rewrites descendant dates when inherited calendars or effort change. Forward those date and duration edits as ordinary task updates. 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 constraint_date, a leaf task with fewer than two of start, end and duration, a dateless summary with no dated children. To get gantt's coercions with a report of each fix, see Accept messy input.
  • week, month and year duration units and calendars with customWeeks are rejected. A gantt merged calendar can contain customWeeks, so such projects can't be imported yet.
  • Dates are naive local date-times with no timezone and no DST. gantt's JS engine uses the host time zone, so its arithmetic follows DST. A Date you pass in is read from its local components, so gantt's Date objects and its formatted strings both work. See Plain objects and dates.
  • result.notes lists the links, tasks and assignments the engine dropped. gantt drops them silently.
  • Three cases give different dates on purpose. With project_constraint: true, children inherit the parent's constraint. gantt's engine ignores it. With use_progress, a completed child of a moving summary stays in place. gantt drags it along. With descendant_links: 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 very large generated graphs the call can throw an Error named Panic. 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.