@jaeungkim/gantt-chart

scheduleTasks

`scheduleTasks`, `SchedulingPolicy`, `ScheduleOptions`, `ScheduleResult`

scheduleTasks takes an array of tasks and returns a new array with every successor moved to fit its dependency links. It is the same function the chart runs on drop, exported from the package root with no React and no DOM involved. What each policy does to a bar you dragged is in Scheduling.

import {
  scheduleTasks,
  type ScheduleOptions,
  type ScheduleResult,
  type SchedulingLink,
  type SchedulingPolicy,
} from '@jaeungkim/gantt-chart';

scheduleTasks

// src/core/scheduling.ts
/**
 * Propagates a move through the dependency graph.
 *
 * One forward pass in topological order: each task is shifted by the largest delta its
 * predecessors demand, then becomes the input for its own successors. Cycles are reported
 * and skipped rather than followed, so this always terminates.
 */
export function scheduleTasks(
  tasks: Task[],
  options: ScheduleOptions = {}
): ScheduleResult
ParameterTypeRequiredMeaning
tasksTask[]yesThe whole project, in any order. Read-only — the array and its elements are never mutated
optionsScheduleOptionsnoDefaults to {}, which means policy: 'off' and therefore no work

SchedulingPolicy

// src/core/scheduling.ts
/**
 * How far a predecessor's move carries into its successors.
 *
 * - `off` - nothing propagates (the default; a chart behaves exactly as it did before)
 * - `shift-on-overlap` - a successor is pushed later only when a link would otherwise be
 *   broken, and is never pulled earlier
 * - `maintain-gap` - a successor sits exactly at its earliest legal date, so it follows the
 *   predecessor in both directions and the gap stays equal to the link's lag
 */
export type SchedulingPolicy = 'off' | 'shift-on-overlap' | 'maintain-gap';

The same three values are accepted by the schedulingPolicy prop. Worked before/after dates for each one are in Scheduling.

ScheduleOptions

// src/core/scheduling.ts
export interface ScheduleOptions {
  policy?: SchedulingPolicy;
  calendar?: WorkingCalendar;
  /**
   * The tasks that just moved. Only their successors are rescheduled, and the seeds
   * themselves are left exactly where the caller put them.
   * Omitted, the whole project is levelled.
   */
  seeds?: Iterable<string>;
  /** Pins summary rows - with hierarchy on their dates come from their children */
  hierarchy?: boolean;
  /** Called with the ids caught in a dependency cycle; those tasks are left alone */
  onCycle?: (taskIds: string[]) => void;
}
OptionTypeDefaultMeaning
policySchedulingPolicy'off'With 'off' the function returns immediately, before the graph is built
calendarWorkingCalendarCALENDAR_DAYSThe day unit every shift, lag and duration is counted in. CALENDAR_DAYS counts all seven days
seedsIterable<string>undefinedTask ids. The walk is restricted to their downstream closure, and the seeds themselves are pinned. Omitted, every task is levelled
hierarchybooleanfalsetrue pins every id that appears as some task's parentId
onCycle(taskIds: string[]) => voidundefinedCalled only when the graph has a cycle — once per run, before any task is moved, with the same array as ScheduleResult.cycle

ScheduleResult

// src/core/scheduling.ts
export interface ScheduleResult {
  /** The same array instance when nothing moved, so callers can skip the update */
  tasks: Task[];
  movedIds: string[];
  cycle: string[] | null;
}
FieldTypeValue
tasksTask[]A new array when at least one task moved; the tasks argument by identity when none did. Task objects that did not move keep their identity in the new array
movedIdsstring[]Ids of the tasks the run rewrote, in the order they were reached. [] when nothing moved
cyclestring[] | nullEvery id that could not be topologically ordered, or null when the graph is acyclic. Always null under policy: 'off' and for an empty tasks array, both of which return before the graph is built

One resolved dependency edge. buildTaskGraph produces these from each task's dependencies array; scheduleTasks consumes them internally, and they reach host code through TaskGraph and linkKey.

// src/core/scheduling.ts
/** One dependency, with both ends resolved */
export interface SchedulingLink {
  predecessorId: string;
  successorId: string;
  type: DependencyType;
  /** Signed, in the calendar's day unit */
  lag: number;
}
FieldTypeValue
predecessorIdstringThe targetId of the TaskDependency it came from
successorIdstringThe id of the task that carries the dependency
typeDependencyType'FS', 'SS', 'FF' or 'SF'. What each anchors is in Dependencies
lagnumberDays, signed. 0 when the source TaskDependency omitted lag. Counted in the calendar's day unit, so 2 under a working calendar means two working days

Example

// schedule.ts - node, no React rendered
import { scheduleTasks, type Task } from '@jaeungkim/gantt-chart';

const tasks: Task[] = [
  {
    id: 'a',
    name: 'Design',
    startDate: '2026-06-02',
    endDate: '2026-06-05',
    parentId: null,
    sequence: '1',
  },
  {
    id: 'b',
    name: 'Build',
    startDate: '2026-06-03',
    endDate: '2026-06-05',
    parentId: null,
    sequence: '2',
    dependencies: [{ targetId: 'a', type: 'FS', lag: 0 }],
  },
];

const result = scheduleTasks(tasks, { policy: 'maintain-gap' });

console.log(result.movedIds); // [ 'b' ]
console.log(result.tasks[1].startDate); // 2026-06-05T00:00:00.000Z
console.log(result.tasks[1].endDate); // 2026-06-07T00:00:00.000Z
console.log(result.cycle); // null

Constraints

policy: 'off' returns { tasks, movedIds: [], cycle: null } without building the graph. cycle is null and onCycle never fires, even when the data does contain a cycle. An empty tasks array takes the same early return.

cycle lists every id Kahn's algorithm could not order. That is wider than the loop itself: a task whose only dependency is on a cycle member appears in cycle too, because it was never reachable in the topological order. The JSDoc on TaskGraph.cycle says "sit on a cycle"; the code reports "could not be ordered".

A task with manuallyScheduled: true is never moved, and its unchanged dates still constrain its successors.

Seeds are pinned before the walk starts, so a seeded task keeps the dates the caller wrote onto it even when they break its own predecessor link.

Every shift moves startDate and endDate by the same number of days. A task's length and its time of day survive propagation; nothing is stretched, shortened or reshaped.

The functions named on this page are all exported from the package root. The helpers the algorithm uses internally — linkDelta, shiftTask, taskStart, taskEnd — are not exported from @jaeungkim/gantt-chart and cannot be imported. The package declares no ./core subpath — its exports are . and ./style.css — so importing scheduleTasks in Node resolves the whole bundle, which lists react and react-dom as peer dependencies; see Headless core.

Slack, early and late dates come from a separate pass — see computeCriticalPath. scheduleTasks reports none of them.

On this page